From 7ee10235638350073453c91adf86826097ae2794 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 3 Jul 2026 20:06:39 +0300 Subject: [PATCH 01/17] Add MeTTa plugin API to allow adding skills and extend prompt --- src/loop.metta | 7 +++++++ src/pluginapi.metta | 28 ++++++++++++++++++++++++++++ src/skills.metta | 8 ++++++++ src/utils.metta | 14 ++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 src/pluginapi.metta diff --git a/src/loop.metta b/src/loop.metta index bbd837a5..34a53591 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -36,6 +36,7 @@ (= (getContext) (string-safe (py-str ("PROMPT: " (getPrompt (provider)) " SKILLS: " (getSkills) + (newline) (getPromptExtensions) (newline) " OUTPUT_FORMAT: Up to 5 lines, do not wrap quotes around args, do not use variables:" (newline) " toolName1 arg1" (newline) " toolName2 arg2" (newline) @@ -47,6 +48,12 @@ " HISTORY: " (getHistory) " TIME: " (get_time_as_string))))) +(= (getPromptExtensions) + (join (newline) (collapse (prompt-extension $_)))) + +; dynamic prompt extension placeholder to eliminate error when no extension is added +(= (prompt-extension placeholder) (empty)) + (= (HandleError $msg $cmd $sexpr) (case $sexpr (((Error $a $b) (let $new (append (get-state &error) (($msg $cmd))) (progn (change-state! &error $new) (ALERT_FAILED $a $b)))) diff --git a/src/pluginapi.metta b/src/pluginapi.metta new file mode 100644 index 00000000..32f25c7a --- /dev/null +++ b/src/pluginapi.metta @@ -0,0 +1,28 @@ +; Add skill to the list of skills available to the agent. +; $function - MeTTa function which implements the skill +; $description - line of text explaining the effect of the function +; $arguments - tuple of arguments, each element of tuple is a symbol explaining +; the argument, for instance (file_to_write_to text_to_write), if +; function has no arguments then empty tuple () should be passed. +(= (add-skill $function $description $arguments) + (add-atom &self (= (dynamic-skill $function) + (py-str ("- " $description ": " $function " " (join " " $arguments)))))) + +; Remove skill from the list of skills available to the agent. +; $function - MeTTa function which implements the skill +(= (remove-skill $function) + (match &self (= (dynamic-skill $function) $text) (remove-atom &self (= (dynamic-skill $function) $text)))) + +; Add new section to the LLM's prompt. The extension is inserted into the +; prompt after SKILL section and before OUTPUT_FORMAT section. +; $handle - symbol to identify and remove the section +; $text - text to add into the prompt +(= (add-prompt-extension $handle $text) + (add-atom &self (= (prompt-extension $handle) $text))) + +; Remove section from the LLM's prompt. +; $handle - symbol to identify the section +(= (remove-prompt-extension $handle) + (match &self (= (prompt-extension $handle) $text) (remove-atom &self (= (prompt-extension $handle) $text)))) + +; FIXME: add MeTTa API to add communication channels and LLM integrations diff --git a/src/skills.metta b/src/skills.metta index 355284f4..7ca22847 100644 --- a/src/skills.metta +++ b/src/skills.metta @@ -1,4 +1,11 @@ (= (getSkills) + (let $static (getStaticSkills) + (collapse (superpose ((superpose $static) (dynamic-skill $_)))))) + +; dynamic skill placeholder to eliminate error when no dynamic skill is added +(= (dynamic-skill placeholder) (empty)) + +(= (getStaticSkills) (;INTERNAL: "- Remember a particular string such as skills and memories: remember string" "- Query long-term embedding memory for skills and memories with short phrases only: query string" @@ -27,6 +34,7 @@ "metta (|~ ((Implication (Inheritance $1 (IntSet Feathered))" " (Inheritance $1 Bird)) (stv 1.0 0.9))" " ((Inheritance Pingu (IntSet Feathered)) (stv 1.0 0.9)))")) + ; FIXME: add load-plugin/unload-plugin skills (= (read-file $file) diff --git a/src/utils.metta b/src/utils.metta index 3e0ba79e..a960f037 100644 --- a/src/utils.metta +++ b/src/utils.metta @@ -70,3 +70,17 @@ (= (command-line) (collapse (argv $_))) + +(= (join $sep $strings) + (if (== $strings ()) + "" + (let* (($head (car-atom $strings)) + ($tail (cdr-atom $strings))) + (join $sep $tail $head)))) + +(= (join $sep $strings $prefix) + (if (== $strings ()) + $prefix + (let* (($head (car-atom $strings)) + ($tail (cdr-atom $strings))) + (join $sep $tail (string_concat (string_concat $prefix $sep) $head))))) From 0fa053c29a68dcd86a85a8046c1e5a9297f30ef5 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 13 Jul 2026 19:16:26 +0300 Subject: [PATCH 02/17] Add MeTTa plugin loader and skills/prompt extension API Add separate loader to load plugins written in MeTTa. Add add-skill/remove-skill add-prompt-extension/remove-prompt-extension MeTTa functions to allow modifying prompt dynamically by plugins. --- lib_omegaclaw.metta | 2 ++ src/plugin.metta | 36 ++++++++++++++++++++++++++++++++++- src/plugin.py | 46 +++++++++++++++++++++++---------------------- src/pluginapi.metta | 21 ++++++++++++++++----- src/utils.metta | 30 +++++++++++++++++++++++++---- 5 files changed, 103 insertions(+), 32 deletions(-) diff --git a/lib_omegaclaw.metta b/lib_omegaclaw.metta index 4090722f..49e104ca 100644 --- a/lib_omegaclaw.metta +++ b/lib_omegaclaw.metta @@ -2,6 +2,7 @@ !(import! &self (library lib_llm)) !(import! &self (library lib_vector)) !(import! &self (library lib_combinatorics)) +!(import! &self (library lib_he)) !(import! &self (library OmegaClaw-Core lib_nal)) !(import! &self (library OmegaClaw-Core lib_pln)) !(import! &self (library OmegaClaw-Core ./providers/lib_llm_ext.py)) @@ -9,6 +10,7 @@ !(import! &self (library OmegaClaw-Core ./src/log)) !(import! &self (library OmegaClaw-Core ./src/utils)) !(import! &self (library OmegaClaw-Core ./src/plugin)) +!(import! &self (library OmegaClaw-Core ./src/pluginapi)) !(import! &self (library OmegaClaw-Core ./src/helper.py)) !(import! &self (library OmegaClaw-Core ./src/agentverse.py)) !(import! &self (library OmegaClaw-Core ./src/channels)) diff --git a/src/plugin.metta b/src/plugin.metta index 1ff1e2e0..4b12d5d0 100644 --- a/src/plugin.metta +++ b/src/plugin.metta @@ -3,7 +3,9 @@ !(import! &self (library OmegaClaw-Core ./src/plugin.py)) (= (initPlugins) - (py-call (plugin.initPlugins))) + (progn + (collapse (loadPlugins (py-call (plugin.listPlugins)))) + ())) (= (commChannelConfig $commchannel) (py-call (plugin.commChannelConfig $commchannel (py-call (plugin.commandLineToDict (command-line)))))) @@ -19,3 +21,35 @@ (= (llmProviderChat $prompt $max_tokens $reasoning_mode) (py-call (plugin.llmProviderChat $prompt $max_tokens $reasoning_mode))) + +; Plugin loader + +(= (loadPlugins $list) + (let* (($head (car-atom $list)) + ($tail (cdr-atom $list))) + (progn + (loadPlugin $head) + (loadPlugins $tail)))) + +(= (loadPlugin (python $name $location)) + (py-call (plugin.loadPythonPlugin $name $location))) + +(= (loadPlugin (metta $name $location)) + (let True (atom-string $location $locationstr) + (loadMettaPlugin $name $locationstr))) + +(= (loadMettaPlugin $name $location) + (if (== $location "") + (log ERROR "plugin" (strings-concat ("Plugin " $name " no location specified"))) + (progn + (let $path (join "/" ($location $name)) ()) + (let $space (atom-concat & $name) ()) + (log INFO "plugin" (strings-concat ("Loading MeTTa plugin " $name " from " $path ".metta"))) + (if (empty-to-bool (import! $space $path)) + (progn + ; FIXME: pass configuration to the plugin initialization function + ; FIXME: fail if loadOmegaClawPlugin returns Error + (eval (loadOmegaClawPlugin)) + (match $space (= (loadOmegaClawPlugin) $body) (remove-atom $space (= (loadOmegaClawPlugin) $body))) + (log INFO "plugin" (strings-concat ("Plugin " $name " is loaded")))) + (log ERROR "plugin" (strings-concat ("Plugin " $name " loading failed"))))))) diff --git a/src/plugin.py b/src/plugin.py index 9418f1c1..d3733e24 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -14,6 +14,7 @@ logger = logging.getLogger(__name__) +_LOADERS = ["python", "metta"] _REPO = pathlib.Path(__file__).parent.parent.resolve() _plugins = {} _commchannel: pluginapi.CommChannel = None @@ -24,34 +25,40 @@ def _error(func, text): logger.error(error) raise RuntimeError(error) -def initPlugins(): +def listPlugins(): """Loads the list of the plugins from ./config/plugins.yaml file and then - loads each plugin from the list using the specified loader. YAML file + returns list of triples (loader, name, location). YAML file contains the list of plugins each specified by three fields: - name - required, must be unique - loader - required, must be either "python" or "metta" - location - optional, path to the plugin module used by Python loader""" - global _plugins, _REPO - - plugins_path = _REPO.joinpath("./config/plugins.yaml") - with open(plugins_path, "r") as f: - plugins = yaml.safe_load(f) + global _plugins, _REPO, _LOADERS - for i, p in enumerate(plugins): + config_path = _REPO.joinpath("./config/plugins.yaml") + with open(config_path, "r") as f: + yaml_content = yaml.safe_load(f) + plugins = [] + for i, p in enumerate(yaml_content): name = p.get("name") if name is None: - _error("initPlugins", f"name field is empty, file: {plugins_path}, index: {i}") + _error("listPlugins", f"name field is empty, file: {config_path}, index: {i}") if name in _plugins: - _error("initPlugins", f"name '{name}' is not unique") + _error("listPlugins", f"name '{name}' is not unique") loader = p.get("loader", "metta") - if loader == "python": - _initPythonPlugin(p) - elif loader == "metta": - _initMettaPlugin(p) + if not loader in _LOADERS: + _error("listPlugins", f"incorrect loader type '{loader}', only {_LOADERS} are supported") + + location = p.get("location") + if location is not None: + location = str(pathlib.Path(location.format(REPO=_REPO)).resolve()) else: - _error("initPlugins", f"loader '{loader}' is not implemented") + location = "" + + plugins.append([loader, name, location]) + + return plugins class PythonPlugin: """Class to wrap the reference to the Python module and keep it inside @@ -60,7 +67,7 @@ class PythonPlugin: def __init__(self, mod): self.mod = mod -def _initPythonPlugin(plugin): +def loadPythonPlugin(name, location): """Python plugin loader implementation. If location of the plugin is specified it imports "/.py" file. Imports Python module otherwise. Calls "loadOmegaClawPlugin" function from the imported @@ -68,10 +75,8 @@ def _initPythonPlugin(plugin): register appropriate callbacks.""" global _plugins, _REPO - name = plugin.get("name") - location = plugin.get("location") mod = None - if location is None: + if not location: logger.info(f"_initPythonPlugin: loading {name} plugin from PYTHONPATH using Python module loader") mod = importlib.import_module(name) else: @@ -95,9 +100,6 @@ def _initPythonPlugin(plugin): plugin_loader = getattr(mod, "loadOmegaClawPlugin") plugin_loader() -def _initMettaPlugin(plugin): - raise NotImplementedError() - def commandLineToDict(list): """Converts list of = pairs into Python dictionary. If parameter doesn't include "=" it is added as a boolean value diff --git a/src/pluginapi.metta b/src/pluginapi.metta index 32f25c7a..c154282b 100644 --- a/src/pluginapi.metta +++ b/src/pluginapi.metta @@ -4,25 +4,36 @@ ; $arguments - tuple of arguments, each element of tuple is a symbol explaining ; the argument, for instance (file_to_write_to text_to_write), if ; function has no arguments then empty tuple () should be passed. +(: add-skill (-> Atom String Expression Bool)) (= (add-skill $function $description $arguments) - (add-atom &self (= (dynamic-skill $function) - (py-str ("- " $description ": " $function " " (join " " $arguments)))))) + (let $text (strings-concat ("- " $description ": " $function " " (join " " $arguments))) + (progn + (log INFO "pluginapi" (strings-concat ("Add skill: " $text))) + (add-atom &self (= (dynamic-skill $function) $text))))) ; Remove skill from the list of skills available to the agent. ; $function - MeTTa function which implements the skill (= (remove-skill $function) - (match &self (= (dynamic-skill $function) $text) (remove-atom &self (= (dynamic-skill $function) $text)))) + (progn + (log INFO "pluginapi" (strings-concat ("Remove skill: " $function))) + (collapse (match &self (= (dynamic-skill $function) $text) (remove-atom &self (= (dynamic-skill $function) $text)))) + True)) ; Add new section to the LLM's prompt. The extension is inserted into the ; prompt after SKILL section and before OUTPUT_FORMAT section. ; $handle - symbol to identify and remove the section ; $text - text to add into the prompt (= (add-prompt-extension $handle $text) - (add-atom &self (= (prompt-extension $handle) $text))) + (progn + (log INFO "pluginapi" (strings-concat ("Add prompt extension " $handle ": " $text))) + (add-atom &self (= (prompt-extension $handle) $text)))) ; Remove section from the LLM's prompt. ; $handle - symbol to identify the section (= (remove-prompt-extension $handle) - (match &self (= (prompt-extension $handle) $text) (remove-atom &self (= (prompt-extension $handle) $text)))) + (progn + (log INFO "pluginapi" (strings-concat ("Remove prompt extension: " $handle))) + (collapse (match &self (= (prompt-extension $handle) $text) (remove-atom &self (= (prompt-extension $handle) $text)))) + True)) ; FIXME: add MeTTa API to add communication channels and LLM integrations diff --git a/src/utils.metta b/src/utils.metta index a960f037..c94d68cf 100644 --- a/src/utils.metta +++ b/src/utils.metta @@ -7,15 +7,27 @@ !(import_prolog_function swrite) !(import_prolog_function sread) !(import_prolog_function string_concat) +!(import_prolog_function make_directory) (= (sleep $t) (progn (translatePredicate (sleep $t)) True)) -(= (exists-file $file) - (case (translatePredicate (exists_file $file)) - (($_ True) - (Empty False)))) +(: empty-to-bool (-> Expression Bool)) +(= (empty-to-bool $predicate) + (case (eval $predicate) + ((Empty False) + ($r $r)))) + +(= (exists-file $path) + (empty-to-bool (translatePredicate (exists_file $path)))) + +(= (exists-directory $path) + (empty-to-bool (translatePredicate (exists_directory $path)))) + +(= (make-directory $path) + (let $rc (catch (((translatePredicate (make_directory $path))))) + (return-on-error $rc True))) (= (string-replace $str $a $b) (atomic_list_concat (split_string $str $a "") $b)) @@ -84,3 +96,13 @@ (let* (($head (car-atom $strings)) ($tail (cdr-atom $strings))) (join $sep $tail (string_concat (string_concat $prefix $sep) $head))))) + +(= (strings-concat $strings) + (join "" $strings)) + +(= (atom-concat $a $b) + (atomic_list_concat ($a $b))) + +(= (atom-string $atom $str) + (empty-to-bool (translatePredicate (atom_string $atom $str)))) + From f9258fddd0390acc3b4487f5bc0ff7b8dafbffed Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 14 Jul 2026 22:05:11 +0300 Subject: [PATCH 03/17] Add unit tests for skill and prompt plugin API --- .github/workflows/common.yml | 4 ++ lib_omegaclaw.metta | 4 +- src/helper.py | 7 +++- src/log.metta | 7 ++++ src/loop.metta | 4 +- src/plugin.metta | 44 +++++++++++---------- src/utils.metta | 9 ++++- tests/lib/utils.metta | 13 +++++++ tests/mettatest.sh | 63 +++++++++++++++++++++++++++++++ tests/res/test_plugin.metta | 2 + tests/res/test_plugin_error.metta | 2 + tests/src_plugin.metta | 22 +++++++++++ tests/src_pluginapi.metta | 25 ++++++++++++ tests/src_utils.metta | 21 +++++++++++ tests/tests_lib_utils.metta | 6 +++ 15 files changed, 207 insertions(+), 26 deletions(-) create mode 100644 tests/lib/utils.metta create mode 100644 tests/mettatest.sh create mode 100644 tests/res/test_plugin.metta create mode 100644 tests/res/test_plugin_error.metta create mode 100644 tests/src_plugin.metta create mode 100644 tests/src_pluginapi.metta create mode 100644 tests/src_utils.metta create mode 100644 tests/tests_lib_utils.metta diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 97e2a9ec..2bac9b25 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -103,6 +103,10 @@ jobs: TEST_SERVER_IP: 'host.docker.internal' IMPORT_KB_ON_START: '0' # TODO: this is turned off because Python dependencies are not installed to the workflow environment + - name: Unit tests + if: inputs.run_tests + run: docker exec -e PETTA_PATH=/PeTTa omegaclaw sh /PeTTa/repos/OmegaClaw-Core/tests/mettatest.sh + - name: Autotests - Phase 1 (Blocking Checks) if: inputs.run_tests working-directory: Autotests diff --git a/lib_omegaclaw.metta b/lib_omegaclaw.metta index 49e104ca..12234ad6 100644 --- a/lib_omegaclaw.metta +++ b/lib_omegaclaw.metta @@ -6,12 +6,12 @@ !(import! &self (library OmegaClaw-Core lib_nal)) !(import! &self (library OmegaClaw-Core lib_pln)) !(import! &self (library OmegaClaw-Core ./providers/lib_llm_ext.py)) +!(import! &self (library OmegaClaw-Core ./src/helper.py)) +!(import! &self (library OmegaClaw-Core ./src/utils)) !(import! &self (library OmegaClaw-Core ./src/logger.py)) !(import! &self (library OmegaClaw-Core ./src/log)) -!(import! &self (library OmegaClaw-Core ./src/utils)) !(import! &self (library OmegaClaw-Core ./src/plugin)) !(import! &self (library OmegaClaw-Core ./src/pluginapi)) -!(import! &self (library OmegaClaw-Core ./src/helper.py)) !(import! &self (library OmegaClaw-Core ./src/agentverse.py)) !(import! &self (library OmegaClaw-Core ./src/channels)) !(import! &self (library OmegaClaw-Core ./src/skills)) diff --git a/src/helper.py b/src/helper.py index 31626303..b582106a 100644 --- a/src/helper.py +++ b/src/helper.py @@ -2,6 +2,7 @@ import json import re from datetime import datetime +import os TS_RE = re.compile(r'^\("(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"') LLM_COMMANDS = { @@ -205,6 +206,11 @@ def normalize_string(x): except Exception: return str(x) +def joinPath(parts): + return os.path.join(*parts) + +def projectRootDirectory(): + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def test_balance_parenthesis(): assert balance_parentheses('(write-file test.txt hello world)') == '((write-file "test.txt" "hello world"))' @@ -230,6 +236,5 @@ def test_balance_parenthesis(): assert balance_parentheses(' ') == '()' assert balance_parentheses('()\nsend hello') == '((send "hello"))' - if __name__ == "__main__": test_balance_parenthesis() diff --git a/src/log.metta b/src/log.metta index 5bfff4fa..034a6c9c 100644 --- a/src/log.metta +++ b/src/log.metta @@ -1,5 +1,12 @@ !(import_prolog_function swrite) +(= (logConfigPath) (empty)) + +(= (initLogger) + (progn + (configure logConfigPath "") + (py-call (logger.setup_logging (logConfigPath))))) + (= (log $type $module $msg) (let $str (swrite $msg) (case $type diff --git a/src/loop.metta b/src/loop.metta index 34a53591..e2cbe107 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -6,7 +6,6 @@ (= (maxOutputToken) (empty)) (= (reasoningMode) (empty)) (= (wakeupInterval) (empty)) -(= (logConfigPath) (empty)) (= (memoryDirectory) (empty)) (= (spamShield) (empty)) ; TODO: this parameter is considered deprecated @@ -20,12 +19,10 @@ (configure maxOutputToken 6000) (configure reasoningMode medium) (configure wakeupInterval 600) ;600=10 minutes - (configure logConfigPath "") (configure memoryDirectory ./) (change-state! &prevmsg "") (change-state! &lastresults "") (change-state! &loops (maxNewInputLoops)) - (py-call (logger.setup_logging (logConfigPath))) )) (= (initKnowledge) @@ -63,6 +60,7 @@ (omegaclaw 1)) (= (omegaclaw $k) (progn (if (== $k 1) (progn (initLoop) + (initLogger) (applySecurityPolicy) (initMemory) (initKnowledge) diff --git a/src/plugin.metta b/src/plugin.metta index 4b12d5d0..ad5d8aff 100644 --- a/src/plugin.metta +++ b/src/plugin.metta @@ -4,7 +4,7 @@ (= (initPlugins) (progn - (collapse (loadPlugins (py-call (plugin.listPlugins)))) + (loadPlugins (py-call (plugin.listPlugins))) ())) (= (commChannelConfig $commchannel) @@ -25,11 +25,7 @@ ; Plugin loader (= (loadPlugins $list) - (let* (($head (car-atom $list)) - ($tail (cdr-atom $list))) - (progn - (loadPlugin $head) - (loadPlugins $tail)))) + (map-atom $list $plugin (loadPlugin $plugin))) (= (loadPlugin (python $name $location)) (py-call (plugin.loadPythonPlugin $name $location))) @@ -39,17 +35,27 @@ (loadMettaPlugin $name $locationstr))) (= (loadMettaPlugin $name $location) - (if (== $location "") - (log ERROR "plugin" (strings-concat ("Plugin " $name " no location specified"))) - (progn - (let $path (join "/" ($location $name)) ()) - (let $space (atom-concat & $name) ()) - (log INFO "plugin" (strings-concat ("Loading MeTTa plugin " $name " from " $path ".metta"))) - (if (empty-to-bool (import! $space $path)) + (let $rc1 + (if (== $location "") + (Error (_loadMettaPlugin $name $location) (strings-concat ("Location is empty"))) (progn - ; FIXME: pass configuration to the plugin initialization function - ; FIXME: fail if loadOmegaClawPlugin returns Error - (eval (loadOmegaClawPlugin)) - (match $space (= (loadOmegaClawPlugin) $body) (remove-atom $space (= (loadOmegaClawPlugin) $body))) - (log INFO "plugin" (strings-concat ("Plugin " $name " is loaded")))) - (log ERROR "plugin" (strings-concat ("Plugin " $name " loading failed"))))))) + (let $path (joinPath ($location $name)) ()) + (let $space (atom-concat & (atom-concat plugin- $name)) ()) + (log INFO "plugin" (strings-concat ("Loading MeTTa plugin " $name " from " $path ".metta"))) + (if (== () (collapse (import! $space $path))) + (Error (_loadMettaPlugin $name $location) (strings-concat ("Cannot import " $name " plugin from " $path))) + (progn + (let $rcs (collapse (eval (loadOmegaClawPlugin))) ()) + (match $space (= (loadOmegaClawPlugin) $body) (remove-atom $space (= (loadOmegaClawPlugin) $body))) + (let $agg (|-> ($acc $next) (if (== $acc ()) (if (== Error (car-atom $next)) $next ()) ())) + (let $rc (foldl-atom $rcs () $agg) ())) + $rc)))) + (progn + (if (= $rc1 (cons Error (cons $_ $error))) + (progn + (log ERROR "plugin" $error) + (log ERROR "plugin" (strings-concat ("Plugin " $name " loading failed"))) + $rc1) + (progn + (log INFO "plugin" (strings-concat ("Plugin " $name " is loaded"))) + ()))))) diff --git a/src/utils.metta b/src/utils.metta index c94d68cf..245a42bd 100644 --- a/src/utils.metta +++ b/src/utils.metta @@ -17,7 +17,7 @@ (= (empty-to-bool $predicate) (case (eval $predicate) ((Empty False) - ($r $r)))) + (True True)))) (= (exists-file $path) (empty-to-bool (translatePredicate (exists_file $path)))) @@ -106,3 +106,10 @@ (= (atom-string $atom $str) (empty-to-bool (translatePredicate (atom_string $atom $str)))) +(= (projectRootDirectory) + (py-call (helper.projectRootDirectory))) + +(= (joinPath $parts) + (progn + (let $path (py-call (helper.joinPath $parts)) ()) + (let True (atom-string $path $pathstr) $pathstr))) diff --git a/tests/lib/utils.metta b/tests/lib/utils.metta new file mode 100644 index 00000000..5b2ea44b --- /dev/null +++ b/tests/lib/utils.metta @@ -0,0 +1,13 @@ +; Testing utility functions + +; Count how many times $expected item is contained in the $list list +(= (expression-count-item $list $expected) + (let $agg (|-> ($res $item) (if (== $item $expected) (+ $res 1) $res)) + (foldl-atom $list 0 $agg))) + +; Check whether $part string is contained inside $text string +(= (contains-text $full $part) + (if (== 1 (py-call (int (py-call (.__contains__ $full $part))))) + True + False)) + diff --git a/tests/mettatest.sh b/tests/mettatest.sh new file mode 100644 index 00000000..48cc4f49 --- /dev/null +++ b/tests/mettatest.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +run_test() { + f="$1" + echo "Running $f" + + full=$(sh "${PETTA_PATH}/run.sh" "${f}") + error=$? + output=$(echo "$full" | grep "is " | grep " should ") + echo "$output" | grep -q "❌" + fail=$? + echo "$output" | grep -q "✅" + pass=$? + if [ $error -ne 0 ] || [ $fail -eq 0 ] || [ $pass -ne 0 ]; then + echo "Full output:" + echo "$full" + echo "FAILURE in $f:" + echo "$output" + return 1 + else + echo "OK: $f" + echo "$output" + return 0 + fi +} + +if [ -z "${PETTA_PATH}" ] || [ ! -r "${PETTA_PATH}/run.sh" ]; then + echo "PETTA_PATH variable should contain the path to the PeTTa directory" + exit 1 +fi + +basedir=$(realpath "$(dirname "$0")") +echo "Basedir $basedir" +cd "${basedir}/.." || exit 1 + +pids="" +pidfile="/tmp/metta_pid_map.$$" +: > "$pidfile" + +for f in ./tests/*.metta; do + run_test "$f" & + pid=$! + pids="$pids $pid" + echo "$pid $f" >> "$pidfile" +done + +status=0 +for pid in $pids; do + if ! wait "$pid"; then + failed_file=$(grep "^$pid " "$pidfile" | cut -d' ' -f2-) + echo "" + echo "===============================" + echo "Stopping tests due to failure:" + echo "❌ Failed test: $failed_file" + echo "===============================" + kill "$pids" 2>/dev/null + status=1 + break + fi +done + +rm -f "$pidfile" +exit $status diff --git a/tests/res/test_plugin.metta b/tests/res/test_plugin.metta new file mode 100644 index 00000000..9a416888 --- /dev/null +++ b/tests/res/test_plugin.metta @@ -0,0 +1,2 @@ +(= (loadOmegaClawPlugin) + (change-state! &test_plugin_loaded True)) diff --git a/tests/res/test_plugin_error.metta b/tests/res/test_plugin_error.metta new file mode 100644 index 00000000..8712f56f --- /dev/null +++ b/tests/res/test_plugin_error.metta @@ -0,0 +1,2 @@ +(= (loadOmegaClawPlugin) + (Error (_loadOmegaClawPlugin) "Plugin internal error")) diff --git a/tests/src_plugin.metta b/tests/src_plugin.metta new file mode 100644 index 00000000..c1decda9 --- /dev/null +++ b/tests/src_plugin.metta @@ -0,0 +1,22 @@ +!(import! &self ./tests/lib/utils) +!(import! &self ./src/utils) +!(import! &self ../src/logger.py) +!(import! &self ./src/log) +!(import! &self ./src/pluginapi) +!(import! &self ./src/plugin) + +!(initLogger) + +!(test (= (cons Error (cons $_ $error)) (Error A B)) True) +!(test (= (cons Error (cons $_ $error)) True) False) + +!(test (loadPlugin (metta "test_plugin" "./tests/res")) ()) + +!(test + (progn + (loadPlugin (metta "test_plugin" "./tests/res")) + (get-state &test_plugin_loaded)) + True) + +!(test (loadPlugin (metta "test_plugin_error" "./tests/res")) + (Error (_loadOmegaClawPlugin) "Plugin internal error")) diff --git a/tests/src_pluginapi.metta b/tests/src_pluginapi.metta new file mode 100644 index 00000000..87854890 --- /dev/null +++ b/tests/src_pluginapi.metta @@ -0,0 +1,25 @@ +!(import! &self ./tests/lib/utils) +!(import! &self ./src/utils) +!(import! &self ./src/pluginapi) +!(import! &self ./src/skills) +!(import! &self ./src/loop) + +!(test (progn + (add-skill test-skill "This is a test skill" (test_arg)) + (expression-count-item (getSkills) "- This is a test skill: test-skill test_arg")) + 1) + +!(test (progn + (remove-skill test-skill) + (expression-count-item (getSkills) "- This is a test skill: test-skill test_arg")) + 0) + +!(test (progn + (add-prompt-extension test-prompt "TEST PROMPT EXTENSION") + (contains-text (getPromptExtensions) "TEST PROMPT EXTENSION")) + True) + +!(test (progn + (remove-prompt-extension test-prompt) + (contains-text (getPromptExtensions) "TEST PROMPT EXTENSION")) + False) diff --git a/tests/src_utils.metta b/tests/src_utils.metta new file mode 100644 index 00000000..8c0dc266 --- /dev/null +++ b/tests/src_utils.metta @@ -0,0 +1,21 @@ +!(import! &self ./tests/lib/utils) +!(import! &self ../src/helper.py) +!(import! &self ./src/utils) + +(= (foo) True) +(= (bar) (empty)) + +!(test (empty-to-bool (foo)) True) +!(test (empty-to-bool (bar)) False) + +;This test relies on mettatest.sh script changes directory to the project root +!(test (projectRootDirectory) + (let True (translatePredicate (absolute_file_name "." $root)) $root)) + +!(test (exists-directory "./not-exist") False) +!(test (exists-directory (projectRootDirectory)) True) + +!(test (joinPath ("a" "b")) + "a/b") +!(test (joinPath ("a/" "b")) + "a/b") diff --git a/tests/tests_lib_utils.metta b/tests/tests_lib_utils.metta new file mode 100644 index 00000000..2bf6d32c --- /dev/null +++ b/tests/tests_lib_utils.metta @@ -0,0 +1,6 @@ +!(import! &self ./tests/lib/utils) + +!(test (expression-count-item (a b c b d) b) 2) + +!(test (contains-text "abcd" "bc") True) +!(test (contains-text "abcd" "de") False) From 1d0363cb2d5541aa42ff6c882c35688cdf0ed6a4 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 16 Jul 2026 17:59:17 +0300 Subject: [PATCH 04/17] Add unit test for Python pluginapi --- pyproject.toml | 4 ++++ tests/test_pluginapi.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 pyproject.toml create mode 100644 tests/test_pluginapi.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..748eaf71 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.pytest.ini_options] +pythonpath = [ + "src" +] diff --git a/tests/test_pluginapi.py b/tests/test_pluginapi.py new file mode 100644 index 00000000..5d45657f --- /dev/null +++ b/tests/test_pluginapi.py @@ -0,0 +1,27 @@ +import pluginapi +import plugin + +class TestCommChannel(pluginapi.CommChannel): + + passed_config = None + + def config(self, config: dict) -> None: + self.passed_config = config + assert self.passed_config["test_param"] == "test_value" + + def receive(self) -> str: + """Receive message from the communication channel""" + raise NotImplementedError() + + def send(self, message: str) -> None: + """Send message via the communication channel""" + raise NotImplementedError() + + +def test_commchannel_config(): + channel = TestCommChannel() + pluginapi.registerCommChannel("Test", channel) + config = { "test_param": "test_value" } + plugin.commChannelConfig("Test", config) + assert channel.passed_config == config + From 375bc92514a2e7a3302ede01c675fc9bf46c314b Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 3 Jul 2026 20:13:09 +0300 Subject: [PATCH 05/17] Add workflow instructions loading plugin This plugin implements functionality introduced by PR #155. It adds agent skills to list available workflows, load and unload workflow instructions. Loading workflow instructions dynamically modifies list of the agent's skills and prompt in order to make agent following the workflow. Co-authored-by: Svetlana Besaeva --- config/plugins.yaml | 4 + instructions/research-workflow/SKILL.md | 103 +++++++++++++++ .../research-workflow/description.txt | 1 + instructions/research-workflow/skills.metta | 119 ++++++++++++++++++ plugins/workflow/utils.metta | 31 +++++ plugins/workflow/utils.pl | 6 + plugins/workflow/utils.py | 35 ++++++ plugins/workflow/workflow.metta | 89 +++++++++++++ 8 files changed, 388 insertions(+) create mode 100644 instructions/research-workflow/SKILL.md create mode 100644 instructions/research-workflow/description.txt create mode 100644 instructions/research-workflow/skills.metta create mode 100644 plugins/workflow/utils.metta create mode 100644 plugins/workflow/utils.pl create mode 100644 plugins/workflow/utils.py create mode 100644 plugins/workflow/workflow.metta diff --git a/config/plugins.yaml b/config/plugins.yaml index e07d1f72..5f0a41cf 100644 --- a/config/plugins.yaml +++ b/config/plugins.yaml @@ -55,3 +55,7 @@ - name: mockprovider loader: python location: "{REPO}/providers" + +- name: workflow + loader: metta + location: "{REPO}/plugins/workflow" diff --git a/instructions/research-workflow/SKILL.md b/instructions/research-workflow/SKILL.md new file mode 100644 index 00000000..2e00e241 --- /dev/null +++ b/instructions/research-workflow/SKILL.md @@ -0,0 +1,103 @@ +--- +name: research-workflow +description: End-to-end research workflow for OmegaClaw. Iterative planning, + data acquisition, experiments, and write-up. +--- +# Research Workflow (OmegaClaw) +This workflow guides you through problem definition, online research, +and creating a detailed execution plan. Once the user approves the plan, +it replaces these instructions and you follow it step by step. +## Project Structure +All projects live under `(let $d (researchDir) (py-str ($d "//")))`. +(researchDir) - is a skill (function) which returns directory the research folders and files should be located +The `research-start` skill creates the base folders. +(researchDir)// + topic.txt # created by research-start + 00_problem.md # research question, scope, metrics + 01_theory.md # data sources, methods, assumptions + 02_plan.md # full execution plan with concrete commands + src/ # all scripts + data/ # raw and processed data + runs/ # configs + metrics (JSON) + figures/ # plots and charts +## Step-by-Step +Next are instructions and MeTTa functions that should be performed step by step +### Step 1 — Define the problem +- Call `(research-start _in_quotes "topic")` to create project folders +- Write research question, scope, success metrics, constraints: + `(let $d (researchDir) (write-file (py-str ($d "//00_problem.md")) "content"))` +- `(research-step _in_quotes "problem-defined" "question: X metric: Y" + "search online for related methods and data sources")` +### Step 2 — Research and create plan +- Search online for related methods and data sources: + `(tavily-search "query")` +- Save findings: + `(let $d (researchDir) (write-file (py-str ($d "//01_theory.md")) "content"))` +- `(research-step _in_quotes "theory-saved" "sources: X methods: Y" + "create plan and present to user")` +- Create the full execution plan using findings and the Plan Template below +- Send plan to user for review: + `(send "PROPOSED PLAN:\n")` +- `(research-checkpoint _in_quotes "Plan ready. Approve or suggest changes?")` +- **WAIT. Do NOT advance until user responds.** +### Step 3 — Plan approval +- If user approves: +save plan: + `(let $d (researchDir) (write-file (py-str ($d "//02_plan.md")) "approved plan text"))` + `(research-step _in_quotes "plan-approved" "milestones A B C" + "follow plan from Milestone A")` +IMPORTANT!!!: load plan to active &active_instructions variable: + `(load-research-dynamic-instructions _in_quotes "02_plan.md")` + take into account that this function receives 2 arguments and "02_plan.md" both in quotes. +- If user requests changes: + Revise plan, send again, wait again +## Plan Template +When writing `02_plan.md`, include ALL sections below. +The plan must be self-contained — after loading it replaces this file. +# Research Plan: +## Operating Rules +- Work autonomously. Make your own decisions on implementation details. +- Use `pin` to track current step between iterations. +- All file paths must be built from `(researchDir)` via `let`: e.g. `(let $d (researchDir) (py-str ($d "//...")))` +- Write code via `write-file`, run via `shell`. Never output code in `send`. +- Save seeds and versions in every script. +- Store metrics as JSON in `runs/`. +- Use `(research-step _in_quotes "step" "result" "next")` after each milestone. +- Do NOT advance past a checkpoint until user responds. +- When to ask the user (batch questions into one message): + - Missing credentials, access, or files + - Ambiguous goal or success metric + - Methodological choice that materially changes results +## Milestone A — Data Ready + Baseline +### A1 — Prepare data and baseline +- `` +- Write: `(let $d (researchDir) (write-file (py-str ($d "//src/baseline.py")) "code"))` + Script: load data, preprocess, simple model, save runs/baseline.json +- Run: `(let $d (researchDir) (shell (py-str ("cd " $d "/ && python src/baseline.py"))))` +- `(research-step _in_quotes "data-ready" "N rows, baseline=X" + "call research-checkpoint")` +- `(research-checkpoint _in_quotes "Data ready. Baseline: X. Proceed?")` +- **WAIT for user.** +## Milestone B — Experiments +### B1 — +- Hypothesis: +- Write: `(let $d (researchDir) (write-file (py-str ($d "//src/experiment_1.py")) "code"))` +- Run: `(let $d (researchDir) (shell (py-str ("cd " $d "/ && python src/experiment_1.py"))))` +- Save: `(let $d (researchDir) (write-file (py-str ($d "//runs/exp1.json")) "metrics"))` +- `(research-step _in_quotes "experiment-1" "metric=Y" "next experiment")` +### B2 — +... +## Milestone C — Results + Conclusions +### C1 — Write results +- `(let $d (researchDir) (write-file (py-str ($d "//05_results.md")) "content"))` +- `(research-step _in_quotes "results-written" "best: X" "write conclusions")` +### C2 — Conclusions +- `(let $d (researchDir) (write-file (py-str ($d "//06_conclusions.md")) "content"))` +- `(research-step _in_quotes "conclusions-done" "summary" + "call research-checkpoint")` +- `(research-checkpoint _in_quotes "Results ready. Next iteration? Proceed?")` +- **WAIT for user.** +## Milestone D — Complete +- `(research-complete _in_quotes)` +## Stop/Pivot Conditions +- `` diff --git a/instructions/research-workflow/description.txt b/instructions/research-workflow/description.txt new file mode 100644 index 00000000..95236b09 --- /dev/null +++ b/instructions/research-workflow/description.txt @@ -0,0 +1 @@ +Every time user asks to start research task, investigate, study, analyze, or build ML model load research-workflow instructions :(load-instructions \"research-workflow\")" diff --git a/instructions/research-workflow/skills.metta b/instructions/research-workflow/skills.metta new file mode 100644 index 00000000..1520a399 --- /dev/null +++ b/instructions/research-workflow/skills.metta @@ -0,0 +1,119 @@ +; Declare skills to be added to the agent skills when workflow is loaded and +; removed on unloading workflow + +(skill research-start "Create research project folders" (research_name_in_quotes topic_in_quotes)) +(skill research-step "Mark research step done" (research_name_in_quotes step_in_quotes result_in_quotes next_action_in_quotes)) +(skill research-checkpoint "Pause for user approval" (researchname_in_quotes message_in_quotes)) +(skill research-complete "Finish research and unload workflow" (researchname_in_quotes)) +(skill load-research-dynamic-instructions " Load a generated file into active context" (researchname_in_quotes filename_in_quotes)) +(skill researchDir "Get research directory" ()) + +;; Actual skills implementation + +;; Build a safe path or empty string if validation fails +(= (safe-instruction-path $project_dir $research-name $filename) + (py-call (helper.safe_path (py-str ($project_dir)) (py-str ($research-name)) (py-str ($filename))))) + +(= (load-research-dynamic-instructions $research-name $filename) + (let $d (researchDir) (load-dynamic-instructions $d $research-name $filename))) + +; loads instructions created during workflow +(= (load-dynamic-instructions $project_dir $research-name $filename) + (if (not (valid-file-or-folder-name $research-name)) + (send (py-str ("ERROR: invalid name: " $research-name))) + (if (not (valid-file-or-folder-name $filename)) + (send (py-str ("ERROR: invalid filename: " $filename))) + (let $path (safe-instruction-path $project_dir $research-name $filename) + (if (== $path "") + (send (py-str ("ERROR: path traversal rejected for " $research-name "/" $filename))) + (load-dynamic-instructions-safe $path $research-name $filename)))))) + +; actually loads the file (internal). Only called after validation. +(= (load-dynamic-instructions-safe $path $research-name $filename) + (if (not (exists-file $path)) + (send (py-str ("ERROR: file not found: " $path))) + (let $content (read-file $path) + (progn + (change-state! &active_instructions $content) + (appendToHistory ((get_time_as_string) + (newline) + (py-str ("LOADED [" $research-name "/" $filename "]")) + (newline))) + (pin (py-str ("ACTIVE: " $research-name + " FILE: " $filename + " STATUS: executing" + " NEXT: follow loaded instructions"))) + (send (py-str ("Loaded " $filename " for " $research-name ". Following instructions."))) + $content)))) + + +(= (researchDir) + ; FIXME: this directory should be created when workspace skill is loaded + (let $dir (get-existing-dir (library memory/skills_workspace)) + (py-str ($dir "/research")))) + + +;; LLM decides to start research +;; creates folders for research and asks to create 00_problem.md then search online +(= (research-start $research-name $topic) + (if (valid-file-or-folder-name $research-name) + (progn (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/src"))) + (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/data"))) + (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/runs"))) + (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/figures"))) + (write-file (py-str ((researchDir) "/" $research-name "/topic.txt")) $topic) + (send (py-str ("Created project: " (researchDir) "/" $research-name "\n" + "Folders: src/ data/ runs/ figures/"))) + (pin (py-str ("RESEARCH_ACTIVE: " $research-name + " DIR: " (researchDir) "/" $research-name + " TOPIC: " $topic + " STEP: 1-problem" + " ACTION_REQUIRED: Write 00_problem.md then search online" + " for related methods and data sources."))) + $research-name) + (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + ) +) + +;; update current step +(= (research-step $research-name $step $result $next) + (if (valid-file-or-folder-name $research-name) + (progn (appendToHistory ((get_time_as_string) (newline) (py-str ("RESEARCH [" $research-name "] done: " $step " — " $result)) (newline)) ) + (send (py-str ("[" $research-name "] done: " $step))) + (pin (py-str ("RESEARCH_ACTIVE: " $research-name + " DONE: " $step + " RESULT: " $result + " NEXT: " $next))) + (py-str ("Done: " $step))) + (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + ) +) + + +;; Checkpoint: wait user's response +(= (research-checkpoint $research-name $message) + (if (valid-file-or-folder-name $research-name) + (progn (pin (py-str ("RESEARCH_ACTIVE: " $research-name + " STATUS: WAITING_FOR_USER" + " DO_NOT_PROCEED" + ))) + (send (py-str ("CHECKPOINT [" $research-name "]\n" $message "\n" + "Reply to continue."))) + "Waiting for user") + (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + ) +) + +;; Unload research skill +(= (research-complete $research-name) + (if (valid-file-or-folder-name $research-name) + (progn (change-state! &active_instructions "") + (send (py-str ("Research complete: " $research-name + "\nResults in " (researchDir) "/" $research-name "/" + "\nWorkflow unloaded from context."))) + (appendToHistory ((get_time_as_string) (newline) (py-str ("RESEARCH [" $research-name "] COMPLETED")) (newline)) ) + (pin "No active research. No active workflow.") + "Research complete, workflow unloaded") + (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + ) +) diff --git a/plugins/workflow/utils.metta b/plugins/workflow/utils.metta new file mode 100644 index 00000000..d7d862cf --- /dev/null +++ b/plugins/workflow/utils.metta @@ -0,0 +1,31 @@ +!(import_prolog_functions_from_file ./utils.pl (safe_exists_directory safe_exists_file)) + +( = (directory-files $dir) + (progn (translatePredicate (directory_files $dir $files)) + $files) +) + +( = (directory-dirs $dir) + (let $f (directory-files $dir) + (filter-dirs $f $dir) + ) +) + + +(= (filter-dirs $files $dir) + (if (== $files ()) + () + (progn (translatePredicate (atom_string (car-atom $files) $first)) + (let* ( + ($tail (cdr-atom $files)) + ($fullpath (py-str ($dir "/" $first))) + ($rest (filter-dirs $tail $dir))) + (if (or (== $first ".") (== $first "..")) + $rest + (if (safe_exists_directory $fullpath) + (cons-atom $first $rest) + $rest)) + ) + ) + ) +) diff --git a/plugins/workflow/utils.pl b/plugins/workflow/utils.pl new file mode 100644 index 00000000..af85216a --- /dev/null +++ b/plugins/workflow/utils.pl @@ -0,0 +1,6 @@ +safe_exists_file(Path, true) :- exists_file(Path), !. +safe_exists_file(_, false). + + +safe_exists_directory(Path, true) :- exists_directory(Path), !. +safe_exists_directory(_, false). diff --git a/plugins/workflow/utils.py b/plugins/workflow/utils.py new file mode 100644 index 00000000..59f9b6d5 --- /dev/null +++ b/plugins/workflow/utils.py @@ -0,0 +1,35 @@ +import os +import re + +def validate_file_or_folder_name(filename: str) -> str: + """ + Validate filename: letters, digits, hyphen, underscore, dot. + Returns "valid" or "invalid". + """ + if not filename: + return "invalid" + if not re.match(r'^[a-zA-Z0-9_.-]+$', filename): + return "invalid" + if filename.startswith('.') or '..' in filename: + return "invalid" + return "valid" + + +def safe_path(base_dir: str, folder: str, filename: str) -> str: + """ + Safely build a file path inside base_dir. + Returns the resolved path or empty string if unsafe. + """ + if validate_file_or_folder_name(folder) != "valid": + return "" + if validate_file_or_folder_name(filename) != "valid": + return "" + + base = os.path.realpath(base_dir) + target = os.path.realpath(os.path.join(base, folder, filename)) + + # Ensure target is inside base directory (prevents path traversal via symlinks) + if not (target == base or target.startswith(base + os.sep)): + return "" + + return target diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta new file mode 100644 index 00000000..92edaf64 --- /dev/null +++ b/plugins/workflow/workflow.metta @@ -0,0 +1,89 @@ +!(import! &self (library lib_spaces)) +!(import! &self (library OmegaClaw-Core src/pluginapi)) +!(import! &self ./utils) +!(import! &self ./utils.py) + +(= (loadOmegaClawPlugin) + (progn + (add-skill workflow-list-available "List available workflows" ()) + (add-skill workflow-load-instructions "Load an existing workflow instructions into the active context" (workflow_name_in_quotes)) + (add-skill workflow-unload-instructions "Unload active workflow from the active context" ()))) + +;; ============ WORKFLOW LOADER ============ + +(= (get-existing-dir $paths) + (let $dir $paths + (if (safe_exists_directory $dir) + $dir + (empty))) +) +;; Validate name +(= (valid-file-or-folder-name $filename) + (if (== (py-str ("valid")) (py-call (utils.validate_file_or_folder_name(py-str ($filename))))) + True + False)) + +; FIXME: replace (instructionsDir) result by plugin parameter to allow user +; configure the location of the workflows collection +(= (instructionsDir) (get-existing-dir (library instructions))) +(= (workflow-load-instructions $name) + (if (not (valid-file-or-folder-name $name)) + (send (py-str ("ERROR: invalid workflow name: " $name))) + (let $path (py-str ((instructionsDir) "/" $name "/SKILL.md")) + (if (not (exists-file $path)) + (send (py-str ("ERROR: skill file not found: " $path))) + (let $content (read-file $path) + (progn + (add-prompt-extension workflow_active_instructions (active-instructions $content)) + (add-workflow-skills $name) + (pin (py-str ("ACTIVE_WORKFLOW: " $name + " STATUS: starting" + " NEXT: follow workflow instructions"))) + (send (py-str ("Loaded workflow: " $name + ". Instructions active in context until completion."))) + (py-str ("Instructions loaded: " $name)))))))) + +(= (workflow-unload-instructions) + (progn (remove-prompt-extension workflow_active_instructions) + (unload-workflow-skills) + (pin "No active workflow. Workflow skills cleared.") + )) + +(= (active-instructions $content) + (py-str (" ACTIVE_WORKFLOW_INSTRUCTIONS: " $content + " FOLLOW THESE INSTRUCTIONS UNTIL THE WORKFLOW IS COMPLETE" + " AND YOU CALL (unload-instructions) OR THE WORKFLOW CALLS ITS COMPLETION FUNCTION." + " EXECUTE ONLY COMMANDS YOU DEEM SAFE; OTHERWISE SKIP OR ASK THE USER."))) + +; to load skills descriptions which are defined only for current workflow +(= (add-workflow-skills $name) + (let $path (py-str ((instructionsDir) "/" $name "/skills")) + (progn + (import! &workflow_skills $path) + (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args)) + (pin (py-str ("Workflow skills loaded: " $name)))))) + +; to load skills descriptions +(= (unload-workflow-skills) + (progn + (match &workflow_skills (skill $func $desc $args) (remove-skill $func)) + (remove-all-atoms &workflow_skills))) + +;;========================== Functions to load workflow description ======================= +;; returns workflow name and its description +(= (workflow-list-available) + (let* ( ($base (instructionsDir)) + ($dirs (directory-dirs $base))) + (collect-description $dirs $base))) + +;; collects workflow description from description.txt +(= (collect-description $dirs $base) + (if (== $dirs () ) + () + (let* (($tail (cdr-atom $dirs)) + ($first (car-atom $dirs)) + ($path (py-str ($base "/" $first "/description.txt"))) + ($rest (collect-description $tail $base))) + (if (safe_exists_file $path) + (cons-atom (py-str ($first ": " (read-file $path))) $rest) + $rest)))) From a58bc54282c39f54e6bdfaa1b2324c63ecb5a53b Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 13 Jul 2026 19:13:43 +0300 Subject: [PATCH 06/17] Unite workflow plugin files into single MeTTa module Because of https://github.com/trueagi-io/PeTTa/issues/196 the only way of writing MeTTa plugins now is using a single MeTTa file. --- plugins/workflow/utils.metta | 31 ------------------- plugins/workflow/utils.pl | 6 ---- plugins/workflow/workflow.metta | 54 ++++++++++++++++++++++++++------- 3 files changed, 43 insertions(+), 48 deletions(-) delete mode 100644 plugins/workflow/utils.metta delete mode 100644 plugins/workflow/utils.pl diff --git a/plugins/workflow/utils.metta b/plugins/workflow/utils.metta deleted file mode 100644 index d7d862cf..00000000 --- a/plugins/workflow/utils.metta +++ /dev/null @@ -1,31 +0,0 @@ -!(import_prolog_functions_from_file ./utils.pl (safe_exists_directory safe_exists_file)) - -( = (directory-files $dir) - (progn (translatePredicate (directory_files $dir $files)) - $files) -) - -( = (directory-dirs $dir) - (let $f (directory-files $dir) - (filter-dirs $f $dir) - ) -) - - -(= (filter-dirs $files $dir) - (if (== $files ()) - () - (progn (translatePredicate (atom_string (car-atom $files) $first)) - (let* ( - ($tail (cdr-atom $files)) - ($fullpath (py-str ($dir "/" $first))) - ($rest (filter-dirs $tail $dir))) - (if (or (== $first ".") (== $first "..")) - $rest - (if (safe_exists_directory $fullpath) - (cons-atom $first $rest) - $rest)) - ) - ) - ) -) diff --git a/plugins/workflow/utils.pl b/plugins/workflow/utils.pl deleted file mode 100644 index af85216a..00000000 --- a/plugins/workflow/utils.pl +++ /dev/null @@ -1,6 +0,0 @@ -safe_exists_file(Path, true) :- exists_file(Path), !. -safe_exists_file(_, false). - - -safe_exists_directory(Path, true) :- exists_directory(Path), !. -safe_exists_directory(_, false). diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 92edaf64..ac0b5b60 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -1,27 +1,30 @@ !(import! &self (library lib_spaces)) -!(import! &self (library OmegaClaw-Core src/pluginapi)) -!(import! &self ./utils) -!(import! &self ./utils.py) +;!(import! &self utils.py) (= (loadOmegaClawPlugin) (progn (add-skill workflow-list-available "List available workflows" ()) (add-skill workflow-load-instructions "Load an existing workflow instructions into the active context" (workflow_name_in_quotes)) - (add-skill workflow-unload-instructions "Unload active workflow from the active context" ()))) + (add-skill workflow-unload-instructions "Unload active workflow from the active context" ()) + )) -;; ============ WORKFLOW LOADER ============ +; ============ WORKFLOW LOADER ============ (= (get-existing-dir $paths) (let $dir $paths - (if (safe_exists_directory $dir) + (if (exists-directory $dir) $dir (empty))) ) ;; Validate name -(= (valid-file-or-folder-name $filename) - (if (== (py-str ("valid")) (py-call (utils.validate_file_or_folder_name(py-str ($filename))))) - True - False)) +; TODO: calling Python validation code is replaced by stub. +; Can be returned back when https://github.com/trueagi-io/PeTTa/issues/196 is +; fixed. +(= (valid-file-or-folder-name $filename) True) +;(= (valid-file-or-folder-name $filename) +; (if (== (py-str ("valid")) (py-call (utils.validate_file_or_folder_name(py-str ($filename))))) +; True +; False)) ; FIXME: replace (instructionsDir) result by plugin parameter to allow user ; configure the location of the workflows collection @@ -84,6 +87,35 @@ ($first (car-atom $dirs)) ($path (py-str ($base "/" $first "/description.txt"))) ($rest (collect-description $tail $base))) - (if (safe_exists_file $path) + (if (exist-file $path) (cons-atom (py-str ($first ": " (read-file $path))) $rest) $rest)))) + +( = (directory-files $dir) + (progn (translatePredicate (directory_files $dir $files)) + $files) +) + +( = (directory-dirs $dir) + (let $f (directory-files $dir) + (filter-dirs $f $dir) + ) +) + +(= (filter-dirs $files $dir) + (if (== $files ()) + () + (progn (translatePredicate (atom_string (car-atom $files) $first)) + (let* ( + ($tail (cdr-atom $files)) + ($fullpath (py-str ($dir "/" $first))) + ($rest (filter-dirs $tail $dir))) + (if (or (== $first ".") (== $first "..")) + $rest + (if (exists-directory $fullpath) + (cons-atom $first $rest) + $rest)) + ) + ) + ) +) From fdb2a52d4831857a23e9887aed43129a70979905 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 16 Jul 2026 13:33:09 +0300 Subject: [PATCH 07/17] Workflow plugin fixes Create workspace for workflows directory on load. Allow configuring instructions directory of the workflow plugin. Add list available workflows to the prompt. --- .../research-workflow/description.txt | 2 +- instructions/research-workflow/skills.metta | 1 - plugins/workflow/workflow.metta | 99 ++++++++++--------- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/instructions/research-workflow/description.txt b/instructions/research-workflow/description.txt index 95236b09..bb84d691 100644 --- a/instructions/research-workflow/description.txt +++ b/instructions/research-workflow/description.txt @@ -1 +1 @@ -Every time user asks to start research task, investigate, study, analyze, or build ML model load research-workflow instructions :(load-instructions \"research-workflow\")" +Every time user asks to start research task, investigate, study, analyze, or build ML model load research-workflow instructions :(workflow-load-instructions \"research-workflow\")" diff --git a/instructions/research-workflow/skills.metta b/instructions/research-workflow/skills.metta index 1520a399..a928db38 100644 --- a/instructions/research-workflow/skills.metta +++ b/instructions/research-workflow/skills.metta @@ -48,7 +48,6 @@ (= (researchDir) - ; FIXME: this directory should be created when workspace skill is loaded (let $dir (get-existing-dir (library memory/skills_workspace)) (py-str ($dir "/research")))) diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index ac0b5b60..3ad63780 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -1,38 +1,51 @@ !(import! &self (library lib_spaces)) -;!(import! &self utils.py) + +(= (pluginWorkflowInstructionsDir) (empty)) (= (loadOmegaClawPlugin) (progn - (add-skill workflow-list-available "List available workflows" ()) - (add-skill workflow-load-instructions "Load an existing workflow instructions into the active context" (workflow_name_in_quotes)) - (add-skill workflow-unload-instructions "Unload active workflow from the active context" ()) - )) - -; ============ WORKFLOW LOADER ============ - -(= (get-existing-dir $paths) - (let $dir $paths - (if (exists-directory $dir) - $dir - (empty))) -) + (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) instructions))) + (if (not (exists-directory (pluginWorkflowInstructionsDir))) + (Error (_loadOmegaClawPlugin) (strings-concat ("Instructions directory: " (pluginWorkflowInstructionsDir) " doesn't exist"))) + (progn + (let $dir (joinPath ((memoryDirectory) skills_workspace)) ()) + (log INFO "workflow-plugin" (strings-concat ("Creating working directory: " $dir))) + (let $rc (make-directory $dir) ()) + (if (exists-directory $dir) + (progn + (log INFO "workflow-plugin" "Adding plugin's skills") + (add-skill workflow-load-instructions "Load an existing workflow instructions into the active context" (workflow_name_in_quotes)) + (add-skill workflow-unload-instructions "Unload active workflow from the active context" ()) + (let $workflows (join (newline) (cons-atom "Available workflows: " (workflow-list-available))) + (add-prompt-extension workflows-available $workflows)) + ()) + $rc))))) + +;; ============ WORKFLOW LOADER ============ + +(= (get-existing-dir $dir) + (if (exists-directory $dir) + $dir + (empty))) + ;; Validate name ; TODO: calling Python validation code is replaced by stub. ; Can be returned back when https://github.com/trueagi-io/PeTTa/issues/196 is ; fixed. (= (valid-file-or-folder-name $filename) True) + +;!(import! &self utils.py) + ;(= (valid-file-or-folder-name $filename) ; (if (== (py-str ("valid")) (py-call (utils.validate_file_or_folder_name(py-str ($filename))))) ; True ; False)) -; FIXME: replace (instructionsDir) result by plugin parameter to allow user -; configure the location of the workflows collection -(= (instructionsDir) (get-existing-dir (library instructions))) +(= (instructionsDir) (get-existing-dir (pluginWorkflowInstructionsDir))) (= (workflow-load-instructions $name) (if (not (valid-file-or-folder-name $name)) (send (py-str ("ERROR: invalid workflow name: " $name))) - (let $path (py-str ((instructionsDir) "/" $name "/SKILL.md")) + (let $path (joinPath ((instructionsDir) $name "SKILL.md")) (if (not (exists-file $path)) (send (py-str ("ERROR: skill file not found: " $path))) (let $content (read-file $path) @@ -60,7 +73,7 @@ ; to load skills descriptions which are defined only for current workflow (= (add-workflow-skills $name) - (let $path (py-str ((instructionsDir) "/" $name "/skills")) + (let $path (joinPath ((instructionsDir) $name "skills")) (progn (import! &workflow_skills $path) (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args)) @@ -80,16 +93,13 @@ (collect-description $dirs $base))) ;; collects workflow description from description.txt -(= (collect-description $dirs $base) - (if (== $dirs () ) - () - (let* (($tail (cdr-atom $dirs)) - ($first (car-atom $dirs)) - ($path (py-str ($base "/" $first "/description.txt"))) - ($rest (collect-description $tail $base))) - (if (exist-file $path) - (cons-atom (py-str ($first ": " (read-file $path))) $rest) - $rest)))) +(= (collect-description () $base) ()) +(= (collect-description (cons $first $tail) $base) + (let* (($path (joinPath ($base $first "description.txt"))) + ($rest (collect-description $tail $base))) + (if (exists-file $path) + (cons-atom (strings-concat ($first ": " (read-file $path))) $rest) + $rest))) ( = (directory-files $dir) (progn (translatePredicate (directory_files $dir $files)) @@ -102,20 +112,17 @@ ) ) -(= (filter-dirs $files $dir) - (if (== $files ()) - () - (progn (translatePredicate (atom_string (car-atom $files) $first)) - (let* ( - ($tail (cdr-atom $files)) - ($fullpath (py-str ($dir "/" $first))) - ($rest (filter-dirs $tail $dir))) - (if (or (== $first ".") (== $first "..")) - $rest - (if (exists-directory $fullpath) - (cons-atom $first $rest) - $rest)) - ) - ) - ) -) +(= (filter-dirs () $dir) ()) +(= (filter-dirs (cons $head $tail) $dir) + (progn (translatePredicate (atom_string $head $first)) + (let* ( + ($fullpath (joinPath ($dir $first))) + ($rest (filter-dirs $tail $dir))) + (if (or (== $first ".") (== $first "..")) + $rest + (if (exists-directory $fullpath) + (cons-atom $first $rest) + $rest)) + ) + ) + ) From 0cb55a5f95e818ea0dd0a6fbf866ab36ab3f9857 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 16 Jul 2026 19:42:43 +0300 Subject: [PATCH 08/17] Move instructions under plugins/workflow directory --- .../workflow/instructions}/research-workflow/SKILL.md | 0 .../workflow/instructions}/research-workflow/description.txt | 0 .../workflow/instructions}/research-workflow/skills.metta | 0 plugins/workflow/workflow.metta | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename {instructions => plugins/workflow/instructions}/research-workflow/SKILL.md (100%) rename {instructions => plugins/workflow/instructions}/research-workflow/description.txt (100%) rename {instructions => plugins/workflow/instructions}/research-workflow/skills.metta (100%) diff --git a/instructions/research-workflow/SKILL.md b/plugins/workflow/instructions/research-workflow/SKILL.md similarity index 100% rename from instructions/research-workflow/SKILL.md rename to plugins/workflow/instructions/research-workflow/SKILL.md diff --git a/instructions/research-workflow/description.txt b/plugins/workflow/instructions/research-workflow/description.txt similarity index 100% rename from instructions/research-workflow/description.txt rename to plugins/workflow/instructions/research-workflow/description.txt diff --git a/instructions/research-workflow/skills.metta b/plugins/workflow/instructions/research-workflow/skills.metta similarity index 100% rename from instructions/research-workflow/skills.metta rename to plugins/workflow/instructions/research-workflow/skills.metta diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 3ad63780..440d4568 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -4,7 +4,7 @@ (= (loadOmegaClawPlugin) (progn - (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) instructions))) + (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) "plugins" "workflow" instructions))) (if (not (exists-directory (pluginWorkflowInstructionsDir))) (Error (_loadOmegaClawPlugin) (strings-concat ("Instructions directory: " (pluginWorkflowInstructionsDir) " doesn't exist"))) (progn From b0d2f8b3c6e8af7f5f9f2b840c91c1bfd164e386 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 16 Jul 2026 20:07:15 +0300 Subject: [PATCH 09/17] Add separate parameter for the workflow memory dir --- .../workflow/instructions/research-workflow/skills.metta | 4 ++-- plugins/workflow/workflow.metta | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/workflow/instructions/research-workflow/skills.metta b/plugins/workflow/instructions/research-workflow/skills.metta index a928db38..14e88bd5 100644 --- a/plugins/workflow/instructions/research-workflow/skills.metta +++ b/plugins/workflow/instructions/research-workflow/skills.metta @@ -48,8 +48,8 @@ (= (researchDir) - (let $dir (get-existing-dir (library memory/skills_workspace)) - (py-str ($dir "/research")))) + (let $dir (get-existing-dir (pluginWorkflowMemoryDir)) + (joinPath ($dir "research")))) ;; LLM decides to start research diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 440d4568..b9f95e81 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -1,6 +1,7 @@ !(import! &self (library lib_spaces)) (= (pluginWorkflowInstructionsDir) (empty)) +(= (pluginWorkflowMemoryDir) (empty)) (= (loadOmegaClawPlugin) (progn @@ -8,10 +9,10 @@ (if (not (exists-directory (pluginWorkflowInstructionsDir))) (Error (_loadOmegaClawPlugin) (strings-concat ("Instructions directory: " (pluginWorkflowInstructionsDir) " doesn't exist"))) (progn - (let $dir (joinPath ((memoryDirectory) skills_workspace)) ()) - (log INFO "workflow-plugin" (strings-concat ("Creating working directory: " $dir))) - (let $rc (make-directory $dir) ()) - (if (exists-directory $dir) + (configure pluginWorkflowMemoryDir (joinPath ((memoryDirectory) workflow_space))) + (log INFO "workflow-plugin" (strings-concat ("Creating working directory: " (pluginWorkflowMemoryDir)))) + (let $rc (make-directory (pluginWorkflowMemoryDir)) ()) + (if (exists-directory (pluginWorkflowMemoryDir)) (progn (log INFO "workflow-plugin" "Adding plugin's skills") (add-skill workflow-load-instructions "Load an existing workflow instructions into the active context" (workflow_name_in_quotes)) From cec51455ca508a0edf0672cddfe50e03671a913b Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 16 Jul 2026 22:02:45 +0300 Subject: [PATCH 10/17] Fix import and instructions dir path --- plugins/workflow/workflow.metta | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index b9f95e81..7f832f63 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -5,7 +5,7 @@ (= (loadOmegaClawPlugin) (progn - (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) "plugins" "workflow" instructions))) + (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) "plugins" "workflow" "instructions"))) (if (not (exists-directory (pluginWorkflowInstructionsDir))) (Error (_loadOmegaClawPlugin) (strings-concat ("Instructions directory: " (pluginWorkflowInstructionsDir) " doesn't exist"))) (progn @@ -77,7 +77,7 @@ (let $path (joinPath ((instructionsDir) $name "skills")) (progn (import! &workflow_skills $path) - (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args)) + (collapse (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args))) (pin (py-str ("Workflow skills loaded: " $name)))))) ; to load skills descriptions From 39b5a345d435f9ccb0f617513aef6eb17e7234e4 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 17 Jul 2026 10:01:24 +0300 Subject: [PATCH 11/17] Fix infinite recursion on loading workflow plugin --- .../research-workflow/skills.metta | 58 ++++++++++--------- plugins/workflow/workflow.metta | 17 +++--- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/plugins/workflow/instructions/research-workflow/skills.metta b/plugins/workflow/instructions/research-workflow/skills.metta index 14e88bd5..0cafab69 100644 --- a/plugins/workflow/instructions/research-workflow/skills.metta +++ b/plugins/workflow/instructions/research-workflow/skills.metta @@ -12,7 +12,11 @@ ;; Build a safe path or empty string if validation fails (= (safe-instruction-path $project_dir $research-name $filename) - (py-call (helper.safe_path (py-str ($project_dir)) (py-str ($research-name)) (py-str ($filename))))) + ; TODO: calling Python validation code is replaced by stub. + ; Can be returned back when https://github.com/trueagi-io/PeTTa/issues/196 is + ; fixed. + ;(py-call (helper.safe_path $project_dir $research-name $filename))) + (py-call (joinPath ($project_dir $research-name $filename)))) (= (load-research-dynamic-instructions $research-name $filename) (let $d (researchDir) (load-dynamic-instructions $d $research-name $filename))) @@ -20,30 +24,30 @@ ; loads instructions created during workflow (= (load-dynamic-instructions $project_dir $research-name $filename) (if (not (valid-file-or-folder-name $research-name)) - (send (py-str ("ERROR: invalid name: " $research-name))) + (send (strings-concat ("ERROR: invalid name: " $research-name))) (if (not (valid-file-or-folder-name $filename)) - (send (py-str ("ERROR: invalid filename: " $filename))) + (send (strings-concat ("ERROR: invalid filename: " $filename))) (let $path (safe-instruction-path $project_dir $research-name $filename) (if (== $path "") - (send (py-str ("ERROR: path traversal rejected for " $research-name "/" $filename))) + (send (strings-concat ("ERROR: path traversal rejected for " $research-name "/" $filename))) (load-dynamic-instructions-safe $path $research-name $filename)))))) ; actually loads the file (internal). Only called after validation. (= (load-dynamic-instructions-safe $path $research-name $filename) (if (not (exists-file $path)) - (send (py-str ("ERROR: file not found: " $path))) + (send (strings-concat ("ERROR: file not found: " $path))) (let $content (read-file $path) (progn (change-state! &active_instructions $content) (appendToHistory ((get_time_as_string) (newline) - (py-str ("LOADED [" $research-name "/" $filename "]")) + (strings-concat ("LOADED [" $research-name "/" $filename "]")) (newline))) - (pin (py-str ("ACTIVE: " $research-name + (pin (strings-concat ("ACTIVE: " $research-name " FILE: " $filename " STATUS: executing" " NEXT: follow loaded instructions"))) - (send (py-str ("Loaded " $filename " for " $research-name ". Following instructions."))) + (send (strings-concat ("Loaded " $filename " for " $research-name ". Following instructions."))) $content)))) @@ -56,35 +60,35 @@ ;; creates folders for research and asks to create 00_problem.md then search online (= (research-start $research-name $topic) (if (valid-file-or-folder-name $research-name) - (progn (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/src"))) - (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/data"))) - (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/runs"))) - (shell (py-str ("mkdir -p " (researchDir) "/" $research-name "/figures"))) - (write-file (py-str ((researchDir) "/" $research-name "/topic.txt")) $topic) - (send (py-str ("Created project: " (researchDir) "/" $research-name "\n" + (progn (shell (strings-concat ("mkdir -p " (researchDir) "/" $research-name "/src"))) + (shell (strings-concat ("mkdir -p " (researchDir) "/" $research-name "/data"))) + (shell (strings-concat ("mkdir -p " (researchDir) "/" $research-name "/runs"))) + (shell (strings-concat ("mkdir -p " (researchDir) "/" $research-name "/figures"))) + (write-file (strings-concat ((researchDir) "/" $research-name "/topic.txt")) $topic) + (send (strings-concat ("Created project: " (researchDir) "/" $research-name "\n" "Folders: src/ data/ runs/ figures/"))) - (pin (py-str ("RESEARCH_ACTIVE: " $research-name + (pin (strings-concat ("RESEARCH_ACTIVE: " $research-name " DIR: " (researchDir) "/" $research-name " TOPIC: " $topic " STEP: 1-problem" " ACTION_REQUIRED: Write 00_problem.md then search online" " for related methods and data sources."))) $research-name) - (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + (send (strings-concat ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) ) ) ;; update current step (= (research-step $research-name $step $result $next) (if (valid-file-or-folder-name $research-name) - (progn (appendToHistory ((get_time_as_string) (newline) (py-str ("RESEARCH [" $research-name "] done: " $step " — " $result)) (newline)) ) - (send (py-str ("[" $research-name "] done: " $step))) - (pin (py-str ("RESEARCH_ACTIVE: " $research-name + (progn (appendToHistory ((get_time_as_string) (newline) (strings-concat ("RESEARCH [" $research-name "] done: " $step " — " $result)) (newline)) ) + (send (strings-concat ("[" $research-name "] done: " $step))) + (pin (strings-concat ("RESEARCH_ACTIVE: " $research-name " DONE: " $step " RESULT: " $result " NEXT: " $next))) - (py-str ("Done: " $step))) - (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + (strings-concat ("Done: " $step))) + (send (strings-concat ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) ) ) @@ -92,14 +96,14 @@ ;; Checkpoint: wait user's response (= (research-checkpoint $research-name $message) (if (valid-file-or-folder-name $research-name) - (progn (pin (py-str ("RESEARCH_ACTIVE: " $research-name + (progn (pin (strings-concat ("RESEARCH_ACTIVE: " $research-name " STATUS: WAITING_FOR_USER" " DO_NOT_PROCEED" ))) - (send (py-str ("CHECKPOINT [" $research-name "]\n" $message "\n" + (send (strings-concat ("CHECKPOINT [" $research-name "]\n" $message "\n" "Reply to continue."))) "Waiting for user") - (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + (send (strings-concat ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) ) ) @@ -107,12 +111,12 @@ (= (research-complete $research-name) (if (valid-file-or-folder-name $research-name) (progn (change-state! &active_instructions "") - (send (py-str ("Research complete: " $research-name + (send (strings-concat ("Research complete: " $research-name "\nResults in " (researchDir) "/" $research-name "/" "\nWorkflow unloaded from context."))) - (appendToHistory ((get_time_as_string) (newline) (py-str ("RESEARCH [" $research-name "] COMPLETED")) (newline)) ) + (appendToHistory ((get_time_as_string) (newline) (strings-concat ("RESEARCH [" $research-name "] COMPLETED")) (newline)) ) (pin "No active research. No active workflow.") "Research complete, workflow unloaded") - (send (py-str ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) + (send (strings-concat ("Invalid name: " $research-name ". Only a-z, 0-9, - and _ allowed."))) ) ) diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 7f832f63..2719f6d1 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -38,27 +38,28 @@ ;!(import! &self utils.py) ;(= (valid-file-or-folder-name $filename) -; (if (== (py-str ("valid")) (py-call (utils.validate_file_or_folder_name(py-str ($filename))))) +; (if (== (strings-concat ("valid")) (py-call +; (utils.validate_file_or_folder_name(strings-concat ($filename))))) ; True ; False)) (= (instructionsDir) (get-existing-dir (pluginWorkflowInstructionsDir))) (= (workflow-load-instructions $name) (if (not (valid-file-or-folder-name $name)) - (send (py-str ("ERROR: invalid workflow name: " $name))) + (send (strings-concat ("ERROR: invalid workflow name: " $name))) (let $path (joinPath ((instructionsDir) $name "SKILL.md")) (if (not (exists-file $path)) - (send (py-str ("ERROR: skill file not found: " $path))) + (send (strings-concat ("ERROR: skill file not found: " $path))) (let $content (read-file $path) (progn (add-prompt-extension workflow_active_instructions (active-instructions $content)) (add-workflow-skills $name) - (pin (py-str ("ACTIVE_WORKFLOW: " $name + (pin (strings-concat ("ACTIVE_WORKFLOW: " $name " STATUS: starting" " NEXT: follow workflow instructions"))) - (send (py-str ("Loaded workflow: " $name + (send (strings-concat ("Loaded workflow: " $name ". Instructions active in context until completion."))) - (py-str ("Instructions loaded: " $name)))))))) + (strings-concat ("Instructions loaded: " $name)))))))) (= (workflow-unload-instructions) (progn (remove-prompt-extension workflow_active_instructions) @@ -67,7 +68,7 @@ )) (= (active-instructions $content) - (py-str (" ACTIVE_WORKFLOW_INSTRUCTIONS: " $content + (strings-concat (" ACTIVE_WORKFLOW_INSTRUCTIONS: " $content " FOLLOW THESE INSTRUCTIONS UNTIL THE WORKFLOW IS COMPLETE" " AND YOU CALL (unload-instructions) OR THE WORKFLOW CALLS ITS COMPLETION FUNCTION." " EXECUTE ONLY COMMANDS YOU DEEM SAFE; OTHERWISE SKIP OR ASK THE USER."))) @@ -78,7 +79,7 @@ (progn (import! &workflow_skills $path) (collapse (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args))) - (pin (py-str ("Workflow skills loaded: " $name)))))) + (pin (strings-concat ("Workflow skills loaded: " $name)))))) ; to load skills descriptions (= (unload-workflow-skills) From 996588675e86f154ff36e04250472d8bd57daae0 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 17 Jul 2026 11:01:01 +0300 Subject: [PATCH 12/17] Unload current workflow from research-complete, fix dynamic instructions --- .../workflow/instructions/research-workflow/skills.metta | 6 +++--- plugins/workflow/workflow.metta | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/workflow/instructions/research-workflow/skills.metta b/plugins/workflow/instructions/research-workflow/skills.metta index 0cafab69..94e32e73 100644 --- a/plugins/workflow/instructions/research-workflow/skills.metta +++ b/plugins/workflow/instructions/research-workflow/skills.metta @@ -15,7 +15,7 @@ ; TODO: calling Python validation code is replaced by stub. ; Can be returned back when https://github.com/trueagi-io/PeTTa/issues/196 is ; fixed. - ;(py-call (helper.safe_path $project_dir $research-name $filename))) + ;(py-call (utils.safe_path $project_dir $research-name $filename))) (py-call (joinPath ($project_dir $research-name $filename)))) (= (load-research-dynamic-instructions $research-name $filename) @@ -38,7 +38,7 @@ (send (strings-concat ("ERROR: file not found: " $path))) (let $content (read-file $path) (progn - (change-state! &active_instructions $content) + (workflow-change-active-instructions $content) (appendToHistory ((get_time_as_string) (newline) (strings-concat ("LOADED [" $research-name "/" $filename "]")) @@ -110,7 +110,7 @@ ;; Unload research skill (= (research-complete $research-name) (if (valid-file-or-folder-name $research-name) - (progn (change-state! &active_instructions "") + (progn (workflow-unload-instructions) (send (strings-concat ("Research complete: " $research-name "\nResults in " (researchDir) "/" $research-name "/" "\nWorkflow unloaded from context."))) diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 2719f6d1..f4f50530 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -87,6 +87,11 @@ (match &workflow_skills (skill $func $desc $args) (remove-skill $func)) (remove-all-atoms &workflow_skills))) +(= (workflow-change-active-instructions $content) + (progn + (remove-prompt-extension workflow_active_instructions) + (add-prompt-extension workflow_active_instructions $content))) + ;;========================== Functions to load workflow description ======================= ;; returns workflow name and its description (= (workflow-list-available) From 6bd0aae6b2a9de209bc92462cdc82b4b8a89c188 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 17 Jul 2026 12:08:27 +0300 Subject: [PATCH 13/17] Add workflow plugin README.md Add workflow plugin description, test workflow to demonstrate the plugin abilities. Rename skills.metta to skill.metta to be more similar with SKILL.md. --- plugins/workflow/README.md | 62 +++++++++++++++++++ .../research-workflow/description.txt | 2 +- .../{skills.metta => skill.metta} | 0 .../instructions/test-workflow/SKILL.md | 10 +++ .../test-workflow/description.txt | 1 + .../instructions/test-workflow/skill.metta | 4 ++ plugins/workflow/workflow.metta | 2 +- 7 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 plugins/workflow/README.md rename plugins/workflow/instructions/research-workflow/{skills.metta => skill.metta} (100%) create mode 100644 plugins/workflow/instructions/test-workflow/SKILL.md create mode 100644 plugins/workflow/instructions/test-workflow/description.txt create mode 100644 plugins/workflow/instructions/test-workflow/skill.metta diff --git a/plugins/workflow/README.md b/plugins/workflow/README.md new file mode 100644 index 00000000..60602224 --- /dev/null +++ b/plugins/workflow/README.md @@ -0,0 +1,62 @@ +# Workflow loading plugin + +Workflow loading plugin allows loading agent skills written according to the +[common specification](https://agentskills.io/specification). Each workflow +consists of short description, detailed description in `SKILL.md` file and +optionally `skill.metta` file which contains related skills implementation in +MeTTa language. + +[Research workflow](./instructions/research-workflow) is a ready-to-use example +of the workflow. One can try it starting the agent and asking it doing a +research on some topic: +``` +Start research. Build a classifier for iris dataset using sklearn, compare +logistic regression and random forest. +``` + +## Workflow files + +`description.txt` (required) contains short usually one-line workflow +description for the agent. For example: +``` +When user asks to demonstrate workflow plugin load test-workflow instructions: (workflow-load-instructions \"test-workflow\") +``` + +`SKILL.md` (required) is an agent skills file in the [common +format](https://agentskills.io/specification). For example: +``` +--- +name: test-workflow +description: Created to check how SKILL.md is loaded to OmegaClaw. +--- +Next are instructions and MeTTa functions that should be performed step by step +# Test Workflow (OmegaClaw) +## Step 1 - demonstrate usage of skills +- Call test-skill with "This is a test workflow demonstration" message +## Step 2 - complete workflow +- Call `(workflow-unload-instructions)` +``` + +`skill.metta` (optional) contains the list of the OmegaClaw skills to load +when workflow is active and additional MeTTa functions which are mentioned in +`SKILL.md` file. + +Skill description are added as high-level expressions. Each such expression +adds one skill to the OmegaClaw. First atom of the expression is `skill` symbol +and other atoms are parameters of the `add-skill` function. See +[pluginapi.metta](/src/pluginapi.metta) for details. Skill implementations are +added as MeTTa functions. + +For example: +``` +(skill test-skill "Test skill to demonstrate workflow by sending message to the user" (message_in_quotes)) + +(= (test-skill $message) + (send $message)) +``` + +## Using workflow + +Start an agent and ask it to use the workflow. For the test workflow above +send: `Demonstrate workflow plugin` + diff --git a/plugins/workflow/instructions/research-workflow/description.txt b/plugins/workflow/instructions/research-workflow/description.txt index bb84d691..41ec836b 100644 --- a/plugins/workflow/instructions/research-workflow/description.txt +++ b/plugins/workflow/instructions/research-workflow/description.txt @@ -1 +1 @@ -Every time user asks to start research task, investigate, study, analyze, or build ML model load research-workflow instructions :(workflow-load-instructions \"research-workflow\")" +Every time user asks to start research task, investigate, study, analyze, or build ML model load research-workflow instructions :(workflow-load-instructions \"research-workflow\") diff --git a/plugins/workflow/instructions/research-workflow/skills.metta b/plugins/workflow/instructions/research-workflow/skill.metta similarity index 100% rename from plugins/workflow/instructions/research-workflow/skills.metta rename to plugins/workflow/instructions/research-workflow/skill.metta diff --git a/plugins/workflow/instructions/test-workflow/SKILL.md b/plugins/workflow/instructions/test-workflow/SKILL.md new file mode 100644 index 00000000..f66a8c97 --- /dev/null +++ b/plugins/workflow/instructions/test-workflow/SKILL.md @@ -0,0 +1,10 @@ +--- +name: test-workflow +description: Created to check how SKILL.md is loaded to OmegaClaw. +--- +Next are instructions and MeTTa functions that should be performed step by step +# Test Workflow (OmegaClaw) +## Step 1 - demonstrate usage of skills +- Call test-skill with "This is a test workflow demonstration" message +## Step 2 - complete workflow +- Call `(workflow-unload-instructions)` diff --git a/plugins/workflow/instructions/test-workflow/description.txt b/plugins/workflow/instructions/test-workflow/description.txt new file mode 100644 index 00000000..7c7235cb --- /dev/null +++ b/plugins/workflow/instructions/test-workflow/description.txt @@ -0,0 +1 @@ +This is a test workflow description, to load the workflow use the command: (workflow-load-instructions \"test-workflow\") diff --git a/plugins/workflow/instructions/test-workflow/skill.metta b/plugins/workflow/instructions/test-workflow/skill.metta new file mode 100644 index 00000000..57b176c1 --- /dev/null +++ b/plugins/workflow/instructions/test-workflow/skill.metta @@ -0,0 +1,4 @@ +(skill test-skill "Test skill to demonstrate workflow by sending message to the user" (message_in_quotes)) + +(= (test-skill $message) + (send $message)) diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index f4f50530..cca8fd1e 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -75,7 +75,7 @@ ; to load skills descriptions which are defined only for current workflow (= (add-workflow-skills $name) - (let $path (joinPath ((instructionsDir) $name "skills")) + (let $path (joinPath ((instructionsDir) $name "skill")) (progn (import! &workflow_skills $path) (collapse (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args))) From 4f68a3af20cdc177846810da3bd80d69399c44ce Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 17 Jul 2026 12:36:14 +0300 Subject: [PATCH 14/17] Add workflow plugin parameters description --- plugins/workflow/README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/workflow/README.md b/plugins/workflow/README.md index 60602224..2b77c007 100644 --- a/plugins/workflow/README.md +++ b/plugins/workflow/README.md @@ -24,7 +24,7 @@ When user asks to demonstrate workflow plugin load test-workflow instructions: ( `SKILL.md` (required) is an agent skills file in the [common format](https://agentskills.io/specification). For example: -``` +```md --- name: test-workflow description: Created to check how SKILL.md is loaded to OmegaClaw. @@ -48,7 +48,7 @@ and other atoms are parameters of the `add-skill` function. See added as MeTTa functions. For example: -``` +```metta (skill test-skill "Test skill to demonstrate workflow by sending message to the user" (message_in_quotes)) (= (test-skill $message) @@ -60,3 +60,18 @@ For example: Start an agent and ask it to use the workflow. For the test workflow above send: `Demonstrate workflow plugin` +## Workflow parameters + +Wofkflow plugin OmegaClaw configuration parameters: +- `pluginWorkflowInstructionsDir` - path to the directory which contains + available workflows. Default value is `/plugins/workflow/instructions` +- `pluginWorkflowMemoryDir` - path to the directory to keep workflow working + files when workflow is active. Default value is `/memory/workflow_space` + +One can set this parameters passing them as a command line arguments. For +example: +```sh +sh run.sh run.metta pluginWorkflowInstructionsDir="" +``` From ea4d5c18df5b81199a0eb5d0020a808487ce7ffd Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 17 Jul 2026 13:02:18 +0300 Subject: [PATCH 15/17] Convert FIXME to TODOs because it will be done in the next PR --- src/pluginapi.metta | 2 +- src/skills.metta | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pluginapi.metta b/src/pluginapi.metta index c154282b..3b863bb7 100644 --- a/src/pluginapi.metta +++ b/src/pluginapi.metta @@ -36,4 +36,4 @@ (collapse (match &self (= (prompt-extension $handle) $text) (remove-atom &self (= (prompt-extension $handle) $text)))) True)) -; FIXME: add MeTTa API to add communication channels and LLM integrations +; TODO add MeTTa API to add communication channels and LLM integrations diff --git a/src/skills.metta b/src/skills.metta index 7ca22847..b61109a9 100644 --- a/src/skills.metta +++ b/src/skills.metta @@ -34,7 +34,7 @@ "metta (|~ ((Implication (Inheritance $1 (IntSet Feathered))" " (Inheritance $1 Bird)) (stv 1.0 0.9))" " ((Inheritance Pingu (IntSet Feathered)) (stv 1.0 0.9)))")) - ; FIXME: add load-plugin/unload-plugin skills + ; TODO add load-plugin/unload-plugin skills (= (read-file $file) From fa5e175aa92f7a8091fdf0099109ad7b48dfc6df Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Sat, 18 Jul 2026 01:19:58 +0300 Subject: [PATCH 16/17] Replace py-str by strings-concat inside research-workflow --- .../instructions/research-workflow/SKILL.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/workflow/instructions/research-workflow/SKILL.md b/plugins/workflow/instructions/research-workflow/SKILL.md index 2e00e241..bd81b4ee 100644 --- a/plugins/workflow/instructions/research-workflow/SKILL.md +++ b/plugins/workflow/instructions/research-workflow/SKILL.md @@ -8,7 +8,7 @@ This workflow guides you through problem definition, online research, and creating a detailed execution plan. Once the user approves the plan, it replaces these instructions and you follow it step by step. ## Project Structure -All projects live under `(let $d (researchDir) (py-str ($d "//")))`. +All projects live under `(let $d (researchDir) (strings-concat ($d "//")))`. (researchDir) - is a skill (function) which returns directory the research folders and files should be located The `research-start` skill creates the base folders. (researchDir)// @@ -25,14 +25,14 @@ Next are instructions and MeTTa functions that should be performed step by ste ### Step 1 — Define the problem - Call `(research-start _in_quotes "topic")` to create project folders - Write research question, scope, success metrics, constraints: - `(let $d (researchDir) (write-file (py-str ($d "//00_problem.md")) "content"))` + `(let $d (researchDir) (write-file (strings-concat ($d "//00_problem.md")) "content"))` - `(research-step _in_quotes "problem-defined" "question: X metric: Y" "search online for related methods and data sources")` ### Step 2 — Research and create plan - Search online for related methods and data sources: `(tavily-search "query")` - Save findings: - `(let $d (researchDir) (write-file (py-str ($d "//01_theory.md")) "content"))` + `(let $d (researchDir) (write-file (strings-concat ($d "//01_theory.md")) "content"))` - `(research-step _in_quotes "theory-saved" "sources: X methods: Y" "create plan and present to user")` - Create the full execution plan using findings and the Plan Template below @@ -43,7 +43,7 @@ Next are instructions and MeTTa functions that should be performed step by ste ### Step 3 — Plan approval - If user approves: save plan: - `(let $d (researchDir) (write-file (py-str ($d "//02_plan.md")) "approved plan text"))` + `(let $d (researchDir) (write-file (strings-concat ($d "//02_plan.md")) "approved plan text"))` `(research-step _in_quotes "plan-approved" "milestones A B C" "follow plan from Milestone A")` IMPORTANT!!!: load plan to active &active_instructions variable: @@ -58,7 +58,7 @@ The plan must be self-contained — after loading it replaces this file. ## Operating Rules - Work autonomously. Make your own decisions on implementation details. - Use `pin` to track current step between iterations. -- All file paths must be built from `(researchDir)` via `let`: e.g. `(let $d (researchDir) (py-str ($d "//...")))` +- All file paths must be built from `(researchDir)` via `let`: e.g. `(let $d (researchDir) (strings-concat ($d "//...")))` - Write code via `write-file`, run via `shell`. Never output code in `send`. - Save seeds and versions in every script. - Store metrics as JSON in `runs/`. @@ -71,9 +71,9 @@ The plan must be self-contained — after loading it replaces this file. ## Milestone A — Data Ready + Baseline ### A1 — Prepare data and baseline - `` -- Write: `(let $d (researchDir) (write-file (py-str ($d "//src/baseline.py")) "code"))` +- Write: `(let $d (researchDir) (write-file (strings-concat ($d "//src/baseline.py")) "code"))` Script: load data, preprocess, simple model, save runs/baseline.json -- Run: `(let $d (researchDir) (shell (py-str ("cd " $d "/ && python src/baseline.py"))))` +- Run: `(let $d (researchDir) (shell (strings-concat ("cd " $d "/ && python src/baseline.py"))))` - `(research-step _in_quotes "data-ready" "N rows, baseline=X" "call research-checkpoint")` - `(research-checkpoint _in_quotes "Data ready. Baseline: X. Proceed?")` @@ -81,18 +81,18 @@ The plan must be self-contained — after loading it replaces this file. ## Milestone B — Experiments ### B1 — - Hypothesis: -- Write: `(let $d (researchDir) (write-file (py-str ($d "//src/experiment_1.py")) "code"))` -- Run: `(let $d (researchDir) (shell (py-str ("cd " $d "/ && python src/experiment_1.py"))))` -- Save: `(let $d (researchDir) (write-file (py-str ($d "//runs/exp1.json")) "metrics"))` +- Write: `(let $d (researchDir) (write-file (strings-concat ($d "//src/experiment_1.py")) "code"))` +- Run: `(let $d (researchDir) (shell (strings-concat ("cd " $d "/ && python src/experiment_1.py"))))` +- Save: `(let $d (researchDir) (write-file (strings-concat ($d "//runs/exp1.json")) "metrics"))` - `(research-step _in_quotes "experiment-1" "metric=Y" "next experiment")` ### B2 — ... ## Milestone C — Results + Conclusions ### C1 — Write results -- `(let $d (researchDir) (write-file (py-str ($d "//05_results.md")) "content"))` +- `(let $d (researchDir) (write-file (strings-concat ($d "//05_results.md")) "content"))` - `(research-step _in_quotes "results-written" "best: X" "write conclusions")` ### C2 — Conclusions -- `(let $d (researchDir) (write-file (py-str ($d "//06_conclusions.md")) "content"))` +- `(let $d (researchDir) (write-file (strings-concat ($d "//06_conclusions.md")) "content"))` - `(research-step _in_quotes "conclusions-done" "summary" "call research-checkpoint")` - `(research-checkpoint _in_quotes "Results ready. Next iteration? Proceed?")` From b168b6226199081d566360882afd4debc9fc6acb Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 21 Jul 2026 10:36:34 +0300 Subject: [PATCH 17/17] Remove py-call --- plugins/workflow/instructions/research-workflow/skill.metta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/workflow/instructions/research-workflow/skill.metta b/plugins/workflow/instructions/research-workflow/skill.metta index 94e32e73..7ff9ddf8 100644 --- a/plugins/workflow/instructions/research-workflow/skill.metta +++ b/plugins/workflow/instructions/research-workflow/skill.metta @@ -16,7 +16,7 @@ ; Can be returned back when https://github.com/trueagi-io/PeTTa/issues/196 is ; fixed. ;(py-call (utils.safe_path $project_dir $research-name $filename))) - (py-call (joinPath ($project_dir $research-name $filename)))) + (joinPath ($project_dir $research-name $filename))) (= (load-research-dynamic-instructions $research-name $filename) (let $d (researchDir) (load-dynamic-instructions $d $research-name $filename)))