diff --git a/config/config.yaml b/config/config.yaml index 9d74a649..8e6143a8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -6,6 +6,8 @@ maxNewInputLoops: 50 maxWakeLoops: 1 # (deprecated) spamShield: False +# Put HISTORY before LAST_SKILL_USE_RESULTS in the prompt so providers can reuse more of the cached prefix +stableContextOrder: False # Delay between loop iterations (seconds) sleepInterval: 1 # LLM model to load overrides *_model parameters see below diff --git a/docs/reference-configuration.md b/docs/reference-configuration.md index 0b9c6262..df50de41 100644 --- a/docs/reference-configuration.md +++ b/docs/reference-configuration.md @@ -16,6 +16,7 @@ This reads a command-line override via `argk` (`name=value` on the MeTTa command |---|---|---| | `maxNewInputLoops` | 50 | How many turns the agent keeps running after a new human message before idling. | | `maxWakeLoops` | 1 | Extra turns granted on each scheduled wake-up. | +| `stableContextOrder` | `False` | Put `HISTORY` before `LAST_SKILL_USE_RESULTS` in the prompt so providers with prefix caching can reuse more of it. | | `sleepInterval` | 1 (seconds) | Delay between loop iterations. | | `LLM` | `gpt-5.4` | Model identifier passed to the provider. | | `provider` | `Anthropic` | LLM provider — `Anthropic`, `OpenAI`, `ASICloud`, or `ASIOne`. | diff --git a/src/helper.py b/src/helper.py index a28ad313..4c9a1952 100644 --- a/src/helper.py +++ b/src/helper.py @@ -200,6 +200,17 @@ def normalize_string(x): logger.debug(f"Could not normalize value, using its plain string form: {e}") return str(x) +def is_true(value): + """Interpret a configuration value as a boolean flag. The value can arrive as + a MeTTa symbol (True/False), a Python bool, or a string from the command line, + an environment variable or the config file. Returns 1 when the value reads as + true and 0 otherwise, so MeTTa can compare the result with (== ... 1).""" + if isinstance(value, bool): + return 1 if value else 0 + if value is None: + return 0 + return 1 if str(value).strip().lower() in ("true", "1", "yes", "on") else 0 + def joinPath(parts): return os.path.join(*parts) diff --git a/src/loop.metta b/src/loop.metta index 783b2daa..91c5c9ec 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -7,11 +7,13 @@ (= (wakeupInterval) (empty)) (= (memoryDirectory) (empty)) (= (spamShield) (empty)) ; TODO: this parameter is considered deprecated +(= (stableContextOrder) (empty)) ; order prompt sections stable->volatile so providers can reuse the cached prefix (= (initLoop) (progn (configure maxNewInputLoops 50) ;20 (configure maxWakeLoops 1) (configure spamShield False) + (configure stableContextOrder False) (configure sleepInterval 1) ;10 (configure provider Anthropic) (configure maxOutputToken 6000) @@ -40,10 +42,36 @@ " toolName4 arg4" (newline) " toolName5 arg5" (newline) " SAVE_PERMANENT_FILES_DIR: " (memoryDirectory) - " LAST_SKILL_USE_RESULTS: " (last_chars (get-state &lastresults) (maxFeedback)) - " HISTORY: " (getHistory) + (contextVolatileTail) " TIME: " (get_time_as_string))))) +; LAST_SKILL_USE_RESULTS changes every cycle and is one of the largest sections, +; so keeping it before HISTORY ends the provider's reusable cache prefix right after +; the static head. With stableContextOrder on, HISTORY (which shifts far less between +; consecutive calls) comes first and the volatile results section goes last, so more of +; the prefix stays byte-identical and can be served from cache. Default is off, in which +; case the tail is byte-for-byte identical to the original layout. +(= (contextVolatileTail) + (py-str (orderedContextSections (stableContextEnabled) + (last_chars (get-state &lastresults) (maxFeedback)) + (getHistory)))) + +; True when the operator has turned stable ordering on. The flag can arrive as a +; string (command line, environment variable or config file all deliver strings), +; a symbol, or a boolean, so normalise it through helper.is_true instead of +; comparing against the True symbol directly. +(= (stableContextEnabled) + (== (py-call (helper.is_true (stableContextOrder))) 1)) + +; Section ordering, kept separate from the state reads and the py-str call so it +; can be checked on its own. Returns the sections as a plain tuple. Flag off keeps +; the results section ahead of HISTORY (the original layout); flag on puts HISTORY +; first so the stable part sits in the reusable cache prefix. +(= (orderedContextSections $stable $results $history) + (if $stable + (" HISTORY: " $history " LAST_SKILL_USE_RESULTS: " $results) + (" LAST_SKILL_USE_RESULTS: " $results " HISTORY: " $history))) + (= (addTelegramPromptExtension) (if (== (commchannel) telegram) (let $path (joinPath ((memoryDirectory) "tg_prompt.txt")) diff --git a/tests/src_loop.metta b/tests/src_loop.metta new file mode 100644 index 00000000..294c262a --- /dev/null +++ b/tests/src_loop.metta @@ -0,0 +1,26 @@ +!(import! &self ./tests/lib/utils) +!(import! &self ../src/helper.py) +!(import! &self ./src/utils) +!(import! &self ./src/loop) + +; orderedContextSections is a pure function of the two section strings and returns +; the sections as a plain tuple, so the ordering can be checked directly without +; loop state, the global flag, or building the final string. + +; Flag off (default): the volatile results section stays ahead of HISTORY, which +; keeps the assembled prompt byte-for-byte identical to the original layout. +!(test (orderedContextSections False "RESULT" "HIST") + (" LAST_SKILL_USE_RESULTS: " "RESULT" " HISTORY: " "HIST")) + +; Flag on: HISTORY moves ahead of the volatile results section. +!(test (orderedContextSections True "RESULT" "HIST") + (" HISTORY: " "HIST" " LAST_SKILL_USE_RESULTS: " "RESULT")) + +; The flag can arrive as a string (command line, environment variable or config +; file all deliver strings), a symbol or a boolean; is_true accepts all of them so +; operators can actually turn it on, not only by editing the default. +!(test (== (py-call (helper.is_true "True")) 1) True) +!(test (== (py-call (helper.is_true "true")) 1) True) +!(test (== (py-call (helper.is_true "False")) 1) False) +!(test (== (py-call (helper.is_true True)) 1) True) +!(test (== (py-call (helper.is_true False)) 1) False)