v0.9.5-pre cleanup, polishing, bugfixing

frdel committed Aug 25, 2025 at 09:59 UTC 0aacdf98bcc08c2bac335fbae65556650e9e49e6
42 files changed +422 -279
.vscode/settings.json
+2 -1
@@ -12,5 +12,6 @@
12 "*": ["webui/*"]
13 },
14 // Optional: point VSCode to jsconfig.json if you add one
15 - "jsconfig.json": "${workspaceFolder}/jsconfig.json"
15 + "jsconfig.json": "${workspaceFolder}/jsconfig.json",
16 + "postman.settings.dotenv-detection-notification-visibility": false
17 }
\ No newline at end of file
agent.py
+27 -21
@@ -1,4 +1,4 @@
1 -import asyncio
1 +import asyncio, random, string
2 import nest_asyncio
3
4 nest_asyncio.apply()
@@ -55,7 +55,7 @@ class AgentContext:
55 last_message: datetime | None = None,
56 ):
57 # build context
58 - self.id = id or str(uuid.uuid4())
58 + self.id = id or AgentContext.generate_id()
59 self.name = name
60 self.config = config
61 self.log = log or Log.Log()
@@ -89,6 +89,15 @@ class AgentContext:
89 def all():
90 return list(AgentContext._contexts.values())
91
92 + @staticmethod
93 + def generate_id():
94 + def generate_short_id():
95 + return ''.join(random.choices(string.ascii_letters + string.digits, k=8))
96 + while True:
97 + short_id = generate_short_id()
98 + if short_id not in AgentContext._contexts:
99 + return short_id
100 +
101 @classmethod
102 def get_notification_manager(cls):
103 if cls._notification_manager is None:
@@ -509,29 +518,27 @@ class Agent:
518 )
519 return system_prompt
520
512 - def parse_prompt(self, file: str, **kwargs):
513 - prompt_dir = files.get_abs_path("prompts")
514 - backup_dir = []
521 + def parse_prompt(self, _prompt_file: str, **kwargs):
522 + dirs = [files.get_abs_path("prompts")]
523 if (
524 self.config.profile
525 ): # if agent has custom folder, use it and use default as backup
526 prompt_dir = files.get_abs_path("agents", self.config.profile, "prompts")
519 - backup_dir.append(files.get_abs_path("prompts"))
527 + dirs.insert(0, prompt_dir)
528 prompt = files.parse_file(
521 - files.get_abs_path(prompt_dir, file), _backup_dirs=backup_dir, **kwargs
529 + _prompt_file, _directories=dirs, **kwargs
530 )
531 return prompt
532
533 def read_prompt(self, file: str, **kwargs) -> str:
526 - prompt_dir = files.get_abs_path("prompts")
527 - backup_dir = []
534 + dirs = [files.get_abs_path("prompts")]
535 if (
536 self.config.profile
537 ): # if agent has custom folder, use it and use default as backup
538 prompt_dir = files.get_abs_path("agents", self.config.profile, "prompts")
532 - backup_dir.append(files.get_abs_path("prompts"))
539 + dirs.insert(0, prompt_dir)
540 prompt = files.read_prompt_file(
534 - files.get_abs_path(prompt_dir, file), _backup_dirs=backup_dir, **kwargs
541 + file, _directories=dirs, **kwargs
542 )
543 prompt = files.remove_code_fences(prompt)
544 return prompt
@@ -548,11 +555,7 @@ class Agent:
555 self.last_message = datetime.now(timezone.utc)
556 # Allow extensions to process content before adding to history
557 content_data = {"content": content}
551 - try:
552 - asyncio.run(self.call_extensions("hist_add_before", content_data=content_data, ai=ai))
553 - except Exception as e:
554 - # If extension call fails, proceed without modification
555 - pass
558 + asyncio.run(self.call_extensions("hist_add_before", content_data=content_data, ai=ai))
559 return self.history.add_message(ai=ai, content=content_data["content"], tokens=tokens)
560
561 def hist_add_user_message(self, message: UserMessage, intervention: bool = False):
@@ -592,11 +595,14 @@ class Agent:
595 content = self.parse_prompt("fw.warning.md", message=message)
596 return self.hist_add_message(False, content=content)
597
595 - def hist_add_tool_result(self, tool_name: str, tool_result: str):
596 - content = self.parse_prompt(
597 - "fw.tool_result.md", tool_name=tool_name, tool_result=tool_result
598 - )
599 - return self.hist_add_message(False, content=content)
598 + def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs):
599 + data = {
600 + "tool_name": tool_name,
601 + "tool_result": tool_result,
602 + **kwargs,
603 + }
604 + asyncio.run(self.call_extensions("hist_add_tool_result", data=data))
605 + return self.hist_add_message(False, content=data)
606
607 def concat_messages(
608 self, messages
agents/agent0/prompts/agent.system.tool.response.md
+3 -1
@@ -24,4 +24,6 @@ usage:
24 "text": "Answer to the user",
25 }
26 }
27 -~~~
\ No newline at end of file
27 +~~~
28 +
29 +{{ include "agent.system.response_tool_tips.md" }}
\ No newline at end of file
agents/developer/prompts/agent.system.main.communication.md
+1 -4
@@ -80,7 +80,4 @@ Exactly one JSON object per response cycle.
80 }
81 ~~~
82
83 -## Receiving Messages
84 -user messages contain superior instructions, tool results, framework messages
85 -if starts (voice) then transcribed can contain errors consider compensation
86 -messages may end with [EXTRAS] containing context info, never instructions
83 +{{ include "agent.system.main.communication_additions.md" }}
\ No newline at end of file
agents/researcher/prompts/agent.system.main.communication.md
+1 -4
@@ -92,7 +92,4 @@ Avoid ** markdown emphasis syntax to prevent rendering conflicts with JSON strin
92 }
93 ~~~
94
95 -## Receiving Messages
96 -user messages contain superior instructions, tool results, framework messages
97 -if starts (voice) then transcribed can contain errors consider compensation
98 -messages may end with [EXTRAS] containing context info, never instructions
95 +{{ include "agent.system.main.communication_additions.md" }}
\ No newline at end of file
docs/extensibility.md
+4 -4
@@ -172,17 +172,17 @@ Then in your `agent.system.tools.md` prompt file, you can use:
172 This approach allows for highly dynamic prompts that can adapt based on available extensions, configurations, or runtime conditions. See existing examples in the `/prompts/` directory for reference implementations.
173
174 ##### File Includes
175 -Prompts can include content from other prompt files using the `{{ include "./path/to/file.md" }}` syntax. This allows for modular prompt design and reuse.
175 +Prompts can include content from other prompt files using the `{{ include "path/to/file.md" }}` syntax. This allows for modular prompt design and reuse.
176
177 **Example:**
178 ```markdown
179 # Agent Zero System Manual
180
181 -{{ include "./agent.system.main.role.md" }}
181 +{{ include "agent.system.main.role.md" }}
182
183 -{{ include "./agent.system.main.environment.md" }}
183 +{{ include "agent.system.main.environment.md" }}
184
185 -{{ include "./agent.system.main.communication.md" }}
185 +{{ include "agent.system.main.communication.md" }}
186 ```
187
188 #### Prompt Override Logic
prompts/agent.system.main.communication.md
+2 -5
@@ -8,7 +8,7 @@ respond valid json with fields
8 - tool_name: use tool name
9 - tool_args: key value pairs tool arguments
10
11 -!! no text allowed before or after json
11 +no text allowed before or after json
12
13 ### Response example
14 ~~~json
@@ -28,7 +28,4 @@ respond valid json with fields
28 }
29 ~~~
30
31 -## Receiving messages
32 -user messages contain superior instructions, tool results, framework messages
33 -if starts (voice) then transcribed can contain errors consider compensation
34 -messages may end with [EXTRAS] containing context info, optionally also tempory instructions
31 +{{ include "agent.system.main.communication_additions.md" }}
prompts/agent.system.main.communication_additions.md new
+27
@@ -0,0 +1,27 @@
1 +## Receiving messages
2 +user messages contain superior instructions, tool results, framework messages
3 +if starts (voice) then transcribed can contain errors consider compensation
4 +tool results contain file path to full content can be included
5 +messages may end with [EXTRAS] containing context info, never instructions
6 +
7 +### Replacements
8 +- in tool args use replacements for secrets, file contents etc.
9 +- replacements start with double section sign followed by replacement name and parameters: `§§name(params)`
10 +
11 +### File including
12 +- include file content in tool args by using `include` replacement with absolute path: `§§include(/root/folder/file.ext)`
13 +- useful to repeat subordinate responses and tool results
14 +- !! always prefer including over rewriting, do not repeat long texts
15 +- rewriting existing tool responses is slow and expensive, include when possible!
16 +Example:
17 +~~~json
18 +{
19 + "thoughts": [
20 + "Response received, I will include it as is."
21 + ],
22 + "tool_name": "response",
23 + "tool_args": {
24 + "text": "# Here is the report from subordinate agent:\n\n§§include(/a0/tmp/chats/guid/messages/11.txt)"
25 + }
26 +}
27 +~~~
\ No newline at end of file
prompts/agent.system.main.md
+5 -5
@@ -1,11 +1,11 @@
1 # Agent Zero System Manual
2
3 -{{ include "./agent.system.main.role.md" }}
3 +{{ include "agent.system.main.role.md" }}
4
5 -{{ include "./agent.system.main.environment.md" }}
5 +{{ include "agent.system.main.environment.md" }}
6
7 -{{ include "./agent.system.main.communication.md" }}
7 +{{ include "agent.system.main.communication.md" }}
8
9 -{{ include "./agent.system.main.solving.md" }}
9 +{{ include "agent.system.main.solving.md" }}
10
11 -{{ include "./agent.system.main.tips.md" }}
11 +{{ include "agent.system.main.tips.md" }}
prompts/agent.system.response_tool_tips.md new
+4
@@ -0,0 +1,4 @@
1 +**tips**
2 +ALWAYS remember to use `§§include(<path>)` replacement to include previous tool results
3 +rewriting text is slow and expensive, include when possible
4 +NEVER rewrite subordinate responses
\ No newline at end of file
prompts/agent.system.secrets.md
+10 -5
@@ -1,6 +1,6 @@
1 # Secret Placeholders
2 -- User secrets are masked and used as placeholders
3 -- Use placeholders in tool calls they will be automatically replaced with actual values
2 +- user secrets are masked and used as aliases
3 +- use aliases in tool calls they will be automatically replaced with actual values
4
5 You have access to the following secrets:
6 <secrets>
@@ -8,7 +8,12 @@ You have access to the following secrets:
8 </secrets>
9
10 ## Important Guidelines:
11 -- Use exact placeholder format §§KEY_NAME§§ double section sign markers
12 -- Values may contain special characters or quotes that may need escaping in code, keep in mind and sanitize in your code if errors occur
13 -- Comments help understand purpose
11 +- use exact alias format `§§secret(key_name)`
12 +- values may contain special characters needing escaping in code, sanitize in your code if errors occur
13 +- comments help understand purpose
14
15 +# Additional variables
16 +- use these non-sensitive variables as they are when needed
17 +<variables>
18 +{{vars}}
19 +</variables>
prompts/agent.system.tool.call_sub.md
+3
@@ -27,5 +27,8 @@ example usage
27 }
28 ~~~
29
30 +**response handling**
31 +- you might be part of long chain of subordinates, avoid slow and expensive rewriting subordinate responses, instead use `§§include(<path>)` alias to include the response as is
32 +
33 **available profiles:**
34 {{agent_profiles}}
\ No newline at end of file
prompts/agent.system.tool.call_sub.py
+2 -1
@@ -14,7 +14,8 @@ class CallSubordinate(VariablesPlugin):
14 for agent_subdir in agent_subdirs:
15 try:
16 context = files.read_prompt_file(
17 - files.get_abs_path("agents", agent_subdir, "_context.md")
17 + "_context.md",
18 + [files.get_abs_path("agents", agent_subdir)]
19 )
20 profiles.append({"name": agent_subdir, "context": context})
21 except Exception as e:
prompts/agent.system.tool.response.md
+2
@@ -15,3 +15,5 @@ usage:
15 }
16 }
17 ~~~
18 +
19 +{{ include "agent.system.response_tool_tips.md" }}
\ No newline at end of file
prompts/agent.system.tools.md
+1 -24
@@ -1,26 +1,3 @@
1 ## Tools available:
2
3 -{{tools}}
4 -
5 -## Remarks about calling tools
6 -Tool calls must always be valid json.
7 -Tool arguments can contain special placeholders:
8 -
9 -### Copy/paste last tool output
10 -- Include the full output of the previous tool call inside any `tool_args` field by inserting the literal token `{last_tool_output}`. It will be replaced automatically before the tool executes.
11 -- Check the [EXTRAS] section for a preview and the originating `tool_name`.
12 -- !! Never repeat the full output of a tool call if it is possible to copy&paste it.
13 -Example for copying the output of the last tool call into the response tool arguments:
14 -~~~json
15 -{
16 - "thoughts": [
17 - "Acknowledge system warning about JSON formatting.",
18 - "Send properly formatted reply using the response tool.",
19 - "Include the previously collected terminal output by inserting the {last_tool_output} token to avoid duplicating raw text manually."
20 - ],
21 - "tool_name": "response",
22 - "tool_args": {
23 - "text": "Here is the terminal output you requested:\n\n{last_tool_output}"
24 - }
25 -}
26 -~~~
3 +{{tools}}
\ No newline at end of file
prompts/fw.extras.last_tool_copy.md deleted
-2
@@ -1,2 +0,0 @@
1 -You can copy and paste the result of the last tool call of {{tool_name}} beginning with "{{last_tool_output_preview}}" into any of the next tool's arguments.
2 -To paste the full content into any next tool's argument, insert the literal token '{last_tool_output}' in the string where you want the content to appear. It will be replaced automatically before the tool runs.
prompts/fw.hint.call_sub.md new
+1
@@ -0,0 +1 @@
1 +do not rewrite long responses, use §§include(<file>) instead!
\ No newline at end of file
python/api/chat_reset.py
+1
@@ -12,6 +12,7 @@ class Reset(ApiHandler):
12 context = self.get_context(ctxid)
13 context.reset()
14 persist_chat.save_tmp_chat(context)
15 + persist_chat.remove_msg_files(ctxid)
16
17 return {
18 "message": "Agent restarted.",
python/extensions/hist_add_tool_result/_90_save_tool_call_file.py new
+37
@@ -0,0 +1,37 @@
1 +from typing import Any
2 +from python.helpers.extension import Extension
3 +from python.helpers import files, persist_chat
4 +import os, re
5 +
6 +LEN_MIN = 500
7 +
8 +class SaveToolCallFile(Extension):
9 + async def execute(self, data: dict[str, Any] | None = None, **kwargs):
10 + if not data:
11 + return
12 +
13 + # get tool call result
14 + result = data.get("tool_result") if isinstance(data, dict) else None
15 + if result is None:
16 + return
17 +
18 + # skip short results
19 + if len(str(result)) < LEN_MIN:
20 + return
21 +
22 + # message files directory
23 + msgs_folder = persist_chat.get_chat_msg_files_folder(self.agent.context.id)
24 + os.makedirs(msgs_folder, exist_ok=True)
25 +
26 + # count the files in the directory
27 + last_num = len(os.listdir(msgs_folder))
28 +
29 + # create new file
30 + new_file = files.get_abs_path(msgs_folder, f"{last_num+1}.txt")
31 + files.write_file(
32 + new_file,
33 + result,
34 + )
35 +
36 + # add the path to the history
37 + data["file"] = new_file
python/extensions/message_loop_prompts_after/_75_include_last_tool_copy_paste_tip.py deleted
-22
@@ -1,22 +0,0 @@
1 -from agent import LoopData
2 -from python.helpers.extension import Extension
3 -
4 -
5 -class IncludeLastToolCopyPasteTip(Extension):
6 - async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
7 - last_call = self.agent.get_data("last_tool_call")
8 - if not last_call:
9 - return
10 -
11 - tool_name: str = last_call.get("tool_name", "")
12 - last_output: str = last_call.get("last_tool_output", "")
13 - if not last_output:
14 - return
15 -
16 - preview = (last_output[:50] or "").replace("\n", " ").strip()
17 - tip = self.agent.read_prompt(
18 - "fw.extras.last_tool_copy.md",
19 - tool_name=tool_name,
20 - last_tool_output_preview=preview,
21 - )
22 - loop_data.extras_temporary["last_tool_copy_paste"] = tip
python/extensions/response_stream/_15_replace_include_alias.py renamed
+10 -11
@@ -1,24 +1,23 @@
1 from typing import Any
2 from python.helpers.extension import Extension
3 +from python.helpers.strings import replace_file_includes
4
5
5 -class ReplaceLastToolOutputInStream(Extension):
6 - async def execute(self, loop_data=None, text: str = "", parsed: dict[str, Any] | None = None, **kwargs):
6 +class ReplaceIncludeAlias(Extension):
7 + async def execute(
8 + self,
9 + loop_data=None,
10 + text: str = "",
11 + parsed: dict[str, Any] | None = None,
12 + **kwargs
13 + ):
14 if not parsed or not isinstance(parsed, dict):
15 return
16
10 - last_call = self.agent.get_data("last_tool_call") or {}
11 - last_output = last_call.get("last_tool_output", "")
12 - if not last_output:
13 - return
14 -
15 - tokens = ("{last_tool_output}", "{{last_tool_output}}")
16 -
17 def replace_placeholders(value: Any) -> Any:
18 if isinstance(value, str):
19 new_val = value
20 - for token in tokens:
21 - new_val = new_val.replace(token, last_output)
20 + new_val = replace_file_includes(new_val, r"§§include\(([^)]+)\)")
21 return new_val
22 if isinstance(value, dict):
23 return {k: replace_placeholders(v) for k, v in value.items()}
python/extensions/system_prompt/_10_system_prompt.py
+3 -1
@@ -2,6 +2,7 @@ from typing import Any
2 from python.helpers.extension import Extension
3 from python.helpers.mcp_handler import MCPConfig
4 from agent import Agent, LoopData
5 +from python.helpers.settings import get_settings
6
7
8 class SystemPrompt(Extension):
@@ -49,7 +50,8 @@ def get_secrets_prompt(agent: Agent):
50 from python.helpers.secrets import SecretsManager
51 secrets_manager = SecretsManager.get_instance()
52 secrets = secrets_manager.get_secrets_for_prompt()
52 - return agent.read_prompt("agent.system.secrets.md", secrets=secrets)
53 + vars = get_settings()["variables"]
54 + return agent.read_prompt("agent.system.secrets.md", secrets=secrets, vars=vars)
55 except Exception as e:
56 # If secrets module is not available or has issues, return empty string
57 return ""
python/extensions/tool_execute_after/_90_store_last_tool_call.py deleted
-26
@@ -1,26 +0,0 @@
1 -from typing import Any
2 -from python.helpers.extension import Extension
3 -
4 -
5 -class StoreLastToolCall(Extension):
6 - async def execute(self, response_data: dict[str, Any] | None = None, tool_name: str = "", tool_args: dict[str, Any] | None = None, **kwargs):
7 - if not response_data:
8 - return
9 -
10 - response = response_data.get("response") if isinstance(response_data, dict) else None
11 - if response is None:
12 - return
13 -
14 - try:
15 - message = getattr(response, "message") if hasattr(response, "message") else str(response)
16 - except Exception:
17 - message = str(response)
18 -
19 - self.agent.set_data(
20 - "last_tool_call",
21 - {
22 - "tool_name": tool_name or "",
23 - "tool_args": tool_args or {},
24 - "last_tool_output": message or "",
25 - },
26 - )
python/helpers/files.py
+50 -62
@@ -1,6 +1,7 @@
1 from abc import ABC, abstractmethod
2 from fnmatch import fnmatch
3 import json
4 +from ntpath import isabs
5 import os
6 import sys
7 import re
@@ -29,10 +30,10 @@ def load_plugin_variables(file: str, backup_dirs: list[str] | None = None) -> di
30 backup_dirs = []
31
32 try:
32 - plugin_file = find_file_in_dirs(
33 - get_abs_path(dirname(file), basename(file, ".md") + ".py"),
34 - backup_dirs
35 - )
33 + # Create filename and directories list
34 + plugin_filename = basename(file, ".md") + ".py"
35 + directories = [dirname(file)] + backup_dirs
36 + plugin_file = find_file_in_dirs(plugin_filename, directories)
37 except FileNotFoundError:
38 plugin_file = None
39
@@ -72,12 +73,12 @@ def load_plugin_variables(file: str, backup_dirs: list[str] | None = None) -> di
73 from python.helpers.strings import sanitize_string
74
75
75 -def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
76 - if _backup_dirs is None:
77 - _backup_dirs = []
76 +def parse_file(_filename: str, _directories: list[str] | None = None, _encoding="utf-8", **kwargs):
77 + if _directories is None:
78 + _directories = []
79
79 - # Try to get the absolute path for the file from the original directory or backup directories
80 - absolute_path = find_file_in_dirs(_relative_path, _backup_dirs)
80 + # Find the file in the directories
81 + absolute_path = find_file_in_dirs(_filename, _directories)
82
83 # Read the file content
84 with open(absolute_path, "r", encoding=_encoding) as f:
@@ -86,7 +87,7 @@ def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
87
88 is_json = is_full_json_template(content)
89 content = remove_code_fences(content)
89 - variables = load_plugin_variables(_relative_path, _backup_dirs) or {} # type: ignore
90 + variables = load_plugin_variables(absolute_path, _directories) or {} # type: ignore
91 variables.update(kwargs)
92 if is_json:
93 content = replace_placeholders_json(content, **variables)
@@ -98,24 +99,30 @@ def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
99 # Process include statements
100 content = process_includes(
101 # here we use kwargs, the plugin variables are not inherited
101 - content, os.path.dirname(_relative_path), _backup_dirs, **kwargs
102 + content, _directories, **kwargs
103 )
104 return content
105
106
106 -def read_prompt_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
107 - if _backup_dirs is None:
108 - _backup_dirs = []
107 +def read_prompt_file(_file: str, _directories: list[str] | None = None, _encoding="utf-8", **kwargs):
108 + if _directories is None:
109 + _directories = []
110
110 - # Try to get the absolute path for the file from the original directory or backup directories
111 - absolute_path = find_file_in_dirs(_relative_path, _backup_dirs)
111 + # If filename contains folder path, extract it and add to directories
112 + if os.path.dirname(_file):
113 + folder_path = os.path.dirname(_file)
114 + _file = os.path.basename(_file)
115 + _directories = [folder_path] + _directories
116 +
117 + # Find the file in the directories
118 + absolute_path = find_file_in_dirs(_file, _directories)
119
120 # Read the file content
121 with open(absolute_path, "r", encoding=_encoding) as f:
122 # content = remove_code_fences(f.read())
123 content = f.read()
124
118 - variables = load_plugin_variables(_relative_path, _backup_dirs) or {} # type: ignore
125 + variables = load_plugin_variables(_file, _directories) or {} # type: ignore
126 variables.update(kwargs)
127
128 # Replace placeholders with values from kwargs
@@ -124,43 +131,33 @@ def read_prompt_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwa
131 # Process include statements
132 content = process_includes(
133 # here we use kwargs, the plugin variables are not inherited
127 - content, os.path.dirname(_relative_path), _backup_dirs, **kwargs
134 + content, _directories, **kwargs
135 )
136
137 return content
138
139
133 -def read_file(relative_path:str, backup_dirs:list[str]|None=None, encoding="utf-8"):
134 - if backup_dirs is None:
135 - backup_dirs = []
136 -
140 +def read_file(relative_path:str, encoding="utf-8"):
141 # Try to get the absolute path for the file from the original directory or backup directories
138 - absolute_path = find_file_in_dirs(relative_path, backup_dirs)
142 + absolute_path = get_abs_path(relative_path)
143
144 # Read the file content
145 with open(absolute_path, "r", encoding=encoding) as f:
146 return f.read()
147
148
145 -def read_file_bin(relative_path:str, backup_dirs:list[str]|None=None):
146 - if backup_dirs is None:
147 - backup_dirs = []
148 -
149 +def read_file_bin(relative_path:str):
150 # Try to get the absolute path for the file from the original directory or backup directories
150 - absolute_path = find_file_in_dirs(relative_path, backup_dirs)
151 + absolute_path = get_abs_path(relative_path)
152
153 # read binary content
154 with open(absolute_path, "rb") as f:
155 return f.read()
156
157
157 -def read_file_base64(relative_path, backup_dirs:list[str]|None=None):
158 - # init backup dirs
159 - if backup_dirs is None:
160 - backup_dirs = []
161 -
158 +def read_file_base64(relative_path):
159 # get absolute path
163 - absolute_path = find_file_in_dirs(relative_path, backup_dirs)
160 + absolute_path = get_abs_path(relative_path)
161
162 # read binary content and encode to base64
163 with open(absolute_path, "rb") as f:
@@ -214,7 +211,7 @@ def replace_placeholders_dict(_content: dict, **kwargs):
211 return replace_value(_content)
212
213
217 -def process_includes(_content, _base_path, _backup_dirs, **kwargs):
214 +def process_includes(_content: str, _directories: list[str], **kwargs):
215 # Regex to find {{ include 'path' }} or {{include'path'}}
216 include_pattern = re.compile(r"{{\s*include\s*['\"](.*?)['\"]\s*}}")
217
@@ -223,41 +220,32 @@ def process_includes(_content, _base_path, _backup_dirs, **kwargs):
220 # if the path is absolute, do not process it
221 if os.path.isabs(include_path):
222 return match.group(0)
226 - # First attempt to resolve the include relative to the base path
227 - full_include_path = find_file_in_dirs(
228 - os.path.join(_base_path, include_path), _backup_dirs
229 - )
230 -
231 - # Recursively read the included file content, keeping the original base path
232 - included_content = read_prompt_file(full_include_path, _backup_dirs, **kwargs)
233 - return included_content
223 + # Search for the include file in the directories
224 + try:
225 + included_content = read_prompt_file(include_path, _directories, **kwargs)
226 + return included_content
227 + except FileNotFoundError:
228 + return match.group(0) # Return original if file not found
229
230 # Replace all includes with the file content
231 return re.sub(include_pattern, replace_include, _content)
232
233
239 -def find_file_in_dirs(file_path, backup_dirs):
234 +def find_file_in_dirs(_filename: str, _directories: list[str]):
235 """
241 - This function tries to find the file first in the given file_path,
242 - and then in the backup_dirs if not found in the original location.
243 - Returns the absolute path of the found file.
236 + This function searches for a filename in a list of directories in order.
237 + Returns the absolute path of the first found file.
238 """
245 - # Try the original path first
246 - if os.path.isfile(get_abs_path(file_path)):
247 - return get_abs_path(file_path)
248 -
249 - # Loop through the backup directories
250 - for backup_dir in backup_dirs:
251 - # backup path should be os.path.join(backup_dir, file_path) but that can lead to problems
252 - # with absolute paths in file_path. So we use basename.
253 - # This means that we don't support include paths like "subdir/something.md" from backups
254 - backup_path = os.path.join(backup_dir, os.path.basename(file_path))
255 - if os.path.isfile(get_abs_path(backup_path)):
256 - return get_abs_path(backup_path)
257 -
258 - # If the file is not found, let it raise the FileNotFoundError
239 + # Loop through the directories in order
240 + for directory in _directories:
241 + # Create full path
242 + full_path = get_abs_path(directory, _filename)
243 + if exists(full_path):
244 + return full_path
245 +
246 + # If the file is not found, raise FileNotFoundError
247 raise FileNotFoundError(
260 - f"File '{file_path}' not found in the original path or backup directories."
248 + f"File '{_filename}' not found in any of the provided directories."
249 )
250
251 def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*"):
python/helpers/history.py
+4
@@ -295,6 +295,7 @@ class History(Record):
295 def __init__(self, agent):
296 from agent import Agent
297
298 + self.counter = 0
299 self.bulks: list[Bulk] = []
300 self.topics: list[Topic] = []
301 self.current = Topic(history=self)
@@ -324,6 +325,7 @@ class History(Record):
325 def add_message(
326 self, ai: bool, content: MessageContent, tokens: int = 0
327 ) -> Message:
328 + self.counter += 1
329 return self.current.add_message(ai, content=content, tokens=tokens)
330
331 def new_topic(self):
@@ -340,6 +342,7 @@ class History(Record):
342
343 @staticmethod
344 def from_dict(data: dict, history: "History"):
345 + history.counter = data.get("counter", 0)
346 history.bulks = [Bulk.from_dict(b, history=history) for b in data["bulks"]]
347 history.topics = [Topic.from_dict(t, history=history) for t in data["topics"]]
348 history.current = Topic.from_dict(data["current"], history=history)
@@ -348,6 +351,7 @@ class History(Record):
351 def to_dict(self):
352 return {
353 "_cls": "History",
354 + "counter": self.counter,
355 "bulks": [b.to_dict() for b in self.bulks],
356 "topics": [t.to_dict() for t in self.topics],
357 "current": self.current.to_dict(),
python/helpers/mcp_handler.py
+29 -29
@@ -156,35 +156,35 @@ class MCPTool(Tool):
156 raw_tool_response = "[Tool returned no textual content]"
157
158 # Prepare user message context
159 - user_message_text = (
160 - "No specific user message context available for this exact step."
161 - )
162 - if (
163 - self.agent
164 - and self.agent.last_user_message
165 - and self.agent.last_user_message.content
166 - ):
167 - content = self.agent.last_user_message.content
168 - if isinstance(content, dict):
169 - # Attempt to get a 'message' field, otherwise stringify the dict
170 - user_message_text = str(content.get(
171 - "message", json.dumps(content, indent=2)
172 - ))
173 - elif isinstance(content, str):
174 - user_message_text = content
175 - else:
176 - # Fallback for any other types (e.g. list, if that were possible for content)
177 - user_message_text = str(content)
178 -
179 - # Ensure user_message_text is a string before length check and slicing
180 - user_message_text = str(user_message_text)
181 -
182 - # Truncate user message context if it's too long to avoid overwhelming the prompt
183 - max_user_context_len = 500 # characters
184 - if len(user_message_text) > max_user_context_len:
185 - user_message_text = (
186 - user_message_text[:max_user_context_len] + "... (truncated)"
187 - )
159 + # user_message_text = (
160 + # "No specific user message context available for this exact step."
161 + # )
162 + # if (
163 + # self.agent
164 + # and self.agent.last_user_message
165 + # and self.agent.last_user_message.content
166 + # ):
167 + # content = self.agent.last_user_message.content
168 + # if isinstance(content, dict):
169 + # # Attempt to get a 'message' field, otherwise stringify the dict
170 + # user_message_text = str(content.get(
171 + # "message", json.dumps(content, indent=2)
172 + # ))
173 + # elif isinstance(content, str):
174 + # user_message_text = content
175 + # else:
176 + # # Fallback for any other types (e.g. list, if that were possible for content)
177 + # user_message_text = str(content)
178 +
179 + # # Ensure user_message_text is a string before length check and slicing
180 + # user_message_text = str(user_message_text)
181 +
182 + # # Truncate user message context if it's too long to avoid overwhelming the prompt
183 + # max_user_context_len = 500 # characters
184 + # if len(user_message_text) > max_user_context_len:
185 + # user_message_text = (
186 + # user_message_text[:max_user_context_len] + "... (truncated)"
187 + # )
188
189 final_text_for_agent = raw_tool_response
190
python/helpers/persist_chat.py
+12 -2
@@ -26,6 +26,8 @@ def get_chat_folder_path(ctxid: str):
26 """
27 return files.get_abs_path(CHATS_FOLDER, ctxid)
28
29 +def get_chat_msg_files_folder(ctxid: str):
30 + return files.get_abs_path(get_chat_folder_path(ctxid), "messages")
31
32 def save_tmp_chat(context: AgentContext):
33 """Save context to the chats folder"""
@@ -107,6 +109,12 @@ def remove_chat(ctxid):
109 files.delete_dir(path)
110
111
112 +def remove_msg_files(ctxid):
113 + """Remove all message files for a chat or task context"""
114 + path = get_chat_msg_files_folder(ctxid)
115 + files.delete_dir(path)
116 +
117 +
118 def _serialize_context(context: AgentContext):
119 # serialize agents
120 agents = []
@@ -119,12 +127,14 @@ def _serialize_context(context: AgentContext):
127 "id": context.id,
128 "name": context.name,
129 "created_at": (
122 - context.created_at.isoformat() if context.created_at
130 + context.created_at.isoformat()
131 + if context.created_at
132 else datetime.fromtimestamp(0).isoformat()
133 ),
134 "type": context.type.value,
135 "last_message": (
127 - context.last_message.isoformat() if context.last_message
136 + context.last_message.isoformat()
137 + if context.last_message
138 else datetime.fromtimestamp(0).isoformat()
139 ),
140 "agents": agents,
python/helpers/secrets.py
+39 -15
@@ -4,13 +4,19 @@ import time
4 import os
5 from io import StringIO
6 from dataclasses import dataclass
7 -from typing import Dict, Optional, List, Literal, Set
7 +from typing import Dict, Optional, List, Literal, Set, Callable
8 from dotenv.parser import parse_stream
9 from python.helpers.errors import RepairableException
10 from python.helpers import files
11
12
13 -KEY_DELIMITER = "§§"
13 +# New alias-based placeholder format §§secret(KEY)
14 +ALIAS_PATTERN = r"§§secret\(([A-Za-z_][A-Za-z0-9_]*)\)"
15 +
16 +def alias_for_key(key: str, placeholder: str = "§§secret({key})") -> str:
17 + # Return alias string for given key in upper-case
18 + key = key.upper()
19 + return placeholder.format(key=key)
20
21 @dataclass
22 class EnvLine:
@@ -27,7 +33,7 @@ class EnvLine:
33 class StreamingSecretsFilter:
34 """Stateful streaming filter that masks secrets on the fly.
35
30 - - Replaces full secret values with placeholders §§KEY§§ when detected.
36 + - Replaces full secret values with placeholders §§secret(KEY) when detected.
37 - Holds the longest suffix of the current buffer that matches any secret prefix
38 (with minimum trigger length of 3) to avoid leaking partial secrets across chunks.
39 - On finalize(), any unresolved partial is masked with '***'.
@@ -59,7 +65,7 @@ class StreamingSecretsFilter:
65 continue
66 key = self.value_to_key.get(val, "")
67 if key:
62 - text = text.replace(val, f"{KEY_DELIMITER}{key}{KEY_DELIMITER}")
68 + text = text.replace(val, alias_for_key(key))
69 return text
70
71 def _longest_suffix_prefix(self, text: str) -> int:
@@ -113,7 +119,7 @@ class StreamingSecretsFilter:
119
120 class SecretsManager:
121 SECRETS_FILE = "tmp/secrets.env"
116 - PLACEHOLDER_PATTERN = rf"{KEY_DELIMITER}([A-Za-z_][A-Za-z0-9_]*){KEY_DELIMITER}"
122 + PLACEHOLDER_PATTERN = ALIAS_PATTERN
123 MASK_VALUE = "***"
124
125 _instance: Optional["SecretsManager"] = None
@@ -224,7 +230,7 @@ class SecretsManager:
230 with_comments=True,
231 with_blank=True,
232 with_other=True,
227 - key_delimiter=KEY_DELIMITER,
233 + key_formatter=alias_for_key,
234 )
235
236 def create_streaming_filter(self) -> "StreamingSecretsFilter":
@@ -246,7 +252,7 @@ class SecretsManager:
252 else:
253 available_keys = ", ".join(secrets.keys())
254 error_msg = (
249 - f"Secret placeholder '{KEY_DELIMITER}{key}{KEY_DELIMITER}' not found in secrets store.\n"
255 + f"Secret placeholder '{alias_for_key(key)}' not found in secrets store.\n"
256 )
257 error_msg += f"Available secrets: {available_keys}"
258
@@ -254,7 +260,23 @@ class SecretsManager:
260
261 return re.sub(self.PLACEHOLDER_PATTERN, replacer, text)
262
257 - def mask_values(self, text: str) -> str:
263 + def change_placeholders(self, text: str, new_format: str) -> str:
264 + """Substitute secret placeholders with a different placeholder format"""
265 + if not text:
266 + return text
267 +
268 + secrets = self.load_secrets()
269 + result = text
270 +
271 + # Sort by length (longest first) to avoid partial replacements
272 + for key, _value in sorted(
273 + secrets.items(), key=lambda x: len(x[1]), reverse=True
274 + ):
275 + result = result.replace(alias_for_key(key), new_format.format(key=key))
276 +
277 + return result
278 +
279 + def mask_values(self, text: str, min_length: int = 4, placeholder: str = "§§secret({key})") -> str:
280 """Replace actual secret values with placeholders in text"""
281 if not text:
282 return text
@@ -266,8 +288,8 @@ class SecretsManager:
288 for key, value in sorted(
289 secrets.items(), key=lambda x: len(x[1]), reverse=True
290 ):
269 - if value and len(value.strip()) > 0:
270 - result = result.replace(value, f"{KEY_DELIMITER}{key}{KEY_DELIMITER}")
291 + if value and len(value.strip()) >= min_length:
292 + result = result.replace(value, alias_for_key(key, placeholder))
293
294 return result
295
@@ -377,17 +399,19 @@ class SecretsManager:
399 with_blank=True,
400 with_other=True,
401 key_delimiter="",
402 + key_formatter: Optional[Callable[[str], str]] = None,
403 ) -> str:
404 out: List[str] = []
405 for ln in lines:
406 if ln.type == "pair" and ln.key is not None:
384 - left = ln.key_part if ln.key_part is not None else ln.key
385 - left = left.upper()
407 + left_raw = ln.key_part if ln.key_part is not None else ln.key
408 + left = left_raw.upper()
409 val = ln.value if ln.value is not None else ""
410 comment = ln.inline_comment or ""
388 - out.append(
389 - f"{key_delimiter}{left}{key_delimiter}{'="'+val+'"' if with_values else ""}{" " + comment if with_comments and comment else ""}"
390 - )
411 + formatted_key = key_formatter(left) if key_formatter else f"{key_delimiter}{left}{key_delimiter}"
412 + val_part = f'="{val}"' if with_values else ""
413 + comment_part = f" {comment}" if with_comments and comment else ""
414 + out.append(f"{formatted_key}{val_part}{comment_part}")
415 elif ln.type == "blank" and with_blank:
416 out.append(ln.raw)
417 elif ln.type == "comment" and with_comments:
python/helpers/settings.py
+12 -1
@@ -103,6 +103,7 @@ class Settings(TypedDict):
103
104 a2a_server_enabled: bool
105
106 + variables: str
107 secrets: str
108
109 class PartialSettings(Settings, total=False):
@@ -1068,10 +1069,19 @@ def convert_out(settings: Settings) -> SettingsOutput:
1069 except Exception:
1070 secrets = ""
1071
1072 + secrets_fields.append({
1073 + "id": "variables",
1074 + "title": "Variables Store",
1075 + "description": "Store non-sensitive variables in .env format e.g. EMAIL_IMAP_SERVER=\"imap.gmail.com\", one item per line. You can use comments starting with # to add descriptions for the agent. See <a href=\"javascript:openModal('settings/secrets/example-vars.html')\">example</a>.<br>These variables are visible to LLMs and in chat history, they are not being masked.",
1076 + "type": "textarea",
1077 + "value": settings["variables"].strip(),
1078 + "style": "height: 20em",
1079 + })
1080 +
1081 secrets_fields.append({
1082 "id": "secrets",
1083 "title": "Secrets Store",
1074 - "description": "Store secrets and credentials in .env format e.g. EMAIL_PASSWORD=\"s3cret-p4$$w0rd\", one item per line. You can use comments starting with # to add descriptions for the agent. See <a href=\"javascript:openModal('settings/secrets/example.html')\">example</a>.",
1084 + "description": "Store secrets and credentials in .env format e.g. EMAIL_PASSWORD=\"s3cret-p4$$w0rd\", one item per line. You can use comments starting with # to add descriptions for the agent. See <a href=\"javascript:openModal('settings/secrets/example-secrets.html')\">example</a>.<br>These variables are not visile to LLMs and in chat history, they are being masked. ⚠️ only values with length >= 4 are being masked to prevent false positives. ",
1085 "type": "textarea",
1086 "value": secrets,
1087 "style": "height: 20em",
@@ -1440,6 +1450,7 @@ def get_default_settings() -> Settings:
1450 mcp_server_enabled=False,
1451 mcp_server_token=create_auth_token(),
1452 a2a_server_enabled=False,
1453 + variables="",
1454 secrets="",
1455 )
1456
python/helpers/strings.py
+19 -1
@@ -1,6 +1,7 @@
1 import re
2 import sys
3 import time
4 +from python.helpers import files
5
6 def sanitize_string(s: str, encoding: str = "utf-8") -> str:
7 # Replace surrogates and invalid unicode with replacement character
@@ -155,4 +156,21 @@ def truncate_text_by_ratio(text: str, threshold: int, replacement: str = "...",
156 # Replace in middle based on ratio
157 start_len = int(available_space * ratio)
158 end_len = available_space - start_len
158 - return text[:start_len] + replacement + text[-end_len:]
\ No newline at end of file
159 + return text[:start_len] + replacement + text[-end_len:]
160 +
161 +
162 +def replace_file_includes(text: str, placeholder_pattern: str = r"§§include\(([^)]+)\)") -> str:
163 + # Replace include aliases with file content
164 + if not text:
165 + return text
166 +
167 + def _repl(match):
168 + path = match.group(1)
169 + try:
170 + # read file content
171 + return files.read_file(path)
172 + except Exception:
173 + # if file not readable keep original placeholder
174 + return match.group(0)
175 +
176 + return re.sub(placeholder_pattern, _repl, text)
\ No newline at end of file
python/helpers/tool.py
+3 -1
@@ -1,5 +1,6 @@
1 from abc import abstractmethod
2 from dataclasses import dataclass
3 +from typing import Any
4
5 from agent import Agent, LoopData
6 from python.helpers.print_style import PrintStyle
@@ -10,6 +11,7 @@ from python.helpers.strings import sanitize_string
11 class Response:
12 message:str
13 break_loop: bool
14 + additional: dict[str, Any] | None = None
15
16 class Tool:
17
@@ -36,7 +38,7 @@ class Tool:
38
39 async def after_execution(self, response: Response, **kwargs):
40 text = sanitize_string(response.message.strip())
39 - self.agent.hist_add_tool_result(self.name, text)
41 + self.agent.hist_add_tool_result(self.name, text, **(response.additional or {}))
42 PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}'")
43 PrintStyle(font_color="#85C1E9").print(text)
44 self.log.update(content=text)
python/tools/a2a_chat.py
+1 -5
@@ -1,10 +1,6 @@
1 from python.helpers.tool import Tool, Response
2 from python.helpers.print_style import PrintStyle
3 -
4 -try:
5 - from python.helpers.fasta2a_client import connect_to_agent, is_client_available # type: ignore
6 -except ImportError: # pragma: no cover – client helper missing
7 - is_client_available = lambda: False # type: ignore
3 +from python.helpers.fasta2a_client import connect_to_agent, is_client_available
4
5
6 class A2AChatTool(Tool):
python/tools/browser_agent.py
+1
@@ -191,6 +191,7 @@ class BrowserAgent(Tool):
191 self.guid = str(uuid.uuid4())
192 reset = str(reset).lower().strip() == "true"
193 await self.prepare_state(reset=reset)
194 + message = SecretsManager.get_instance().mask_values(message, placeholder="<secret>{key}</secret>") # mask any potential passwords passed from A0 to browser-use to browser-use format
195 task = self.state.start_task(message) if self.state else None
196
197 # wait for browser agent to finish and update progress with timeout
python/tools/call_subordinate.py
+11 -5
@@ -1,6 +1,7 @@
1 from agent import Agent, UserMessage
2 from python.helpers.tool import Tool, Response
3 from initialize import initialize_agent
4 +from python.extensions.hist_add_tool_result import _90_save_tool_call_file as save_tool_call_file
5
6
7 class Delegation(Tool):
@@ -26,16 +27,21 @@ class Delegation(Tool):
27 self.agent.set_data(Agent.DATA_NAME_SUBORDINATE, sub)
28
29 # add user message to subordinate agent
29 - subordinate: Agent = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE) # type: ignore
30 + subordinate: Agent = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE) # type: ignore
31 subordinate.hist_add_user_message(UserMessage(message=message, attachments=[]))
32
32 -
33 -
33 # run subordinate monologue
34 result = await subordinate.monologue()
35
36 + # hint to use includes for long responses
37 + additional = None
38 + if len(result) >= save_tool_call_file.LEN_MIN:
39 + hint = self.agent.read_prompt("fw.hint.call_sub.md")
40 + if hint:
41 + additional = {"hint": hint}
42 +
43 # result
38 - return Response(message=result, break_loop=False)
44 + return Response(message=result, break_loop=False, additional=additional)
45
46 def get_log_object(self):
47 return self.agent.context.log.log(
@@ -43,4 +49,4 @@ class Delegation(Tool):
49 heading=f"icon://communication {self.agent.agent_name}: Calling Subordinate Agent",
50 content="",
51 kvps=self.args,
46 - )
\ No newline at end of file
52 + )
python/tools/code_execution_tool.py
+2 -2
@@ -74,7 +74,7 @@ class CodeExecution(Tool):
74 return f"icon://terminal {session_text}{text}"
75
76 async def after_execution(self, response, **kwargs):
77 - self.agent.hist_add_tool_result(self.name, response.message)
77 + self.agent.hist_add_tool_result(self.name, response.message, **(response.additional or {}))
78
79 async def prepare_state(self, reset=False, session: int | None = None):
80 self.state: State | None = self.agent.get_data("_cet_state")
@@ -372,5 +372,5 @@ class CodeExecution(Tool):
372 output = re.sub(r"(?<!\\)\\x[0-9A-Fa-f]{2}", "", output)
373 # Strip every line of output before truncation
374 output = "\n".join(line.strip() for line in output.splitlines())
375 - output = truncate_text_agent(agent=self.agent, output=output, threshold=10000)
375 + output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000) # ~1MB, larger outputs should be dumped to file, not read from terminal
376 return output
python/tools/input.py
+1 -1
@@ -23,4 +23,4 @@ class Input(Tool):
23 return self.agent.context.log.log(type="code_exe", heading=f"icon://keyboard {self.agent.agent_name}: Using tool '{self.name}'", content="", kvps=self.args)
24
25 async def after_execution(self, response, **kwargs):
26 - self.agent.hist_add_tool_result(self.name, response.message)
\ No newline at end of file
26 + self.agent.hist_add_tool_result(self.name, response.message, **(response.additional or {}))
\ No newline at end of file
python/tools/scheduler.py
+14 -14
@@ -60,7 +60,7 @@ class SchedulerTool(Tool):
60 return Response(message=json.dumps(filtered_tasks, indent=4), break_loop=False)
61
62 async def find_task_by_name(self, **kwargs) -> Response:
63 - name: str = kwargs.get("name", None)
63 + name: str = kwargs.get("name", "")
64 if not name:
65 return Response(message="Task name is required", break_loop=False)
66 tasks: list[ScheduledTask | AdHocTask | PlannedTask] = TaskScheduler.get().find_task_by_name(name)
@@ -69,7 +69,7 @@ class SchedulerTool(Tool):
69 return Response(message=json.dumps([serialize_task(task) for task in tasks], indent=4), break_loop=False)
70
71 async def show_task(self, **kwargs) -> Response:
72 - task_uuid: str = kwargs.get("uuid", None)
72 + task_uuid: str = kwargs.get("uuid", "")
73 if not task_uuid:
74 return Response(message="Task UUID is required", break_loop=False)
75 task: ScheduledTask | AdHocTask | PlannedTask | None = TaskScheduler.get().get_task_by_uuid(task_uuid)
@@ -78,7 +78,7 @@ class SchedulerTool(Tool):
78 return Response(message=json.dumps(serialize_task(task), indent=4), break_loop=False)
79
80 async def run_task(self, **kwargs) -> Response:
81 - task_uuid: str = kwargs.get("uuid", None)
81 + task_uuid: str = kwargs.get("uuid", "")
82 if not task_uuid:
83 return Response(message="Task UUID is required", break_loop=False)
84 task_context: str | None = kwargs.get("context", None)
@@ -93,7 +93,7 @@ class SchedulerTool(Tool):
93 return Response(message=f"Task started: {task_uuid}", break_loop=break_loop)
94
95 async def delete_task(self, **kwargs) -> Response:
96 - task_uuid: str = kwargs.get("uuid", None)
96 + task_uuid: str = kwargs.get("uuid", "")
97 if not task_uuid:
98 return Response(message="Task UUID is required", break_loop=False)
99
@@ -133,9 +133,9 @@ class SchedulerTool(Tool):
133 # "month": "*",
134 # "weekday": "*",
135 # }
136 - name: str = kwargs.get("name", None)
137 - system_prompt: str = kwargs.get("system_prompt", None)
138 - prompt: str = kwargs.get("prompt", None)
136 + name: str = kwargs.get("name", "")
137 + system_prompt: str = kwargs.get("system_prompt", "")
138 + prompt: str = kwargs.get("prompt", "")
139 attachments: list[str] = kwargs.get("attachments", [])
140 schedule: dict[str, str] = kwargs.get("schedule", {})
141 dedicated_context: bool = kwargs.get("dedicated_context", False)
@@ -165,9 +165,9 @@ class SchedulerTool(Tool):
165 return Response(message=f"Scheduled task '{name}' created: {task.uuid}", break_loop=False)
166
167 async def create_adhoc_task(self, **kwargs) -> Response:
168 - name: str = kwargs.get("name", None)
169 - system_prompt: str = kwargs.get("system_prompt", None)
170 - prompt: str = kwargs.get("prompt", None)
168 + name: str = kwargs.get("name", "")
169 + system_prompt: str = kwargs.get("system_prompt", "")
170 + prompt: str = kwargs.get("prompt", "")
171 attachments: list[str] = kwargs.get("attachments", [])
172 token: str = str(random.randint(1000000000000000000, 9999999999999999999))
173 dedicated_context: bool = kwargs.get("dedicated_context", False)
@@ -184,9 +184,9 @@ class SchedulerTool(Tool):
184 return Response(message=f"Adhoc task '{name}' created: {task.uuid}", break_loop=False)
185
186 async def create_planned_task(self, **kwargs) -> Response:
187 - name: str = kwargs.get("name", None)
188 - system_prompt: str = kwargs.get("system_prompt", None)
189 - prompt: str = kwargs.get("prompt", None)
187 + name: str = kwargs.get("name", "")
188 + system_prompt: str = kwargs.get("system_prompt", "")
189 + prompt: str = kwargs.get("prompt", "")
190 attachments: list[str] = kwargs.get("attachments", [])
191 plan: list[str] = kwargs.get("plan", [])
192 dedicated_context: bool = kwargs.get("dedicated_context", False)
@@ -219,7 +219,7 @@ class SchedulerTool(Tool):
219 return Response(message=f"Planned task '{name}' created: {task.uuid}", break_loop=False)
220
221 async def wait_for_task(self, **kwargs) -> Response:
222 - task_uuid: str = kwargs.get("uuid", None)
222 + task_uuid: str = kwargs.get("uuid", "")
223 if not task_uuid:
224 return Response(message="Task UUID is required", break_loop=False)
225
webui/components/_examples/_example-store.js
+4
@@ -8,6 +8,10 @@ const model = {
8 // gets called when the store is created
9 init(){
10 console.log("Example store initialized");
11 + },
12 +
13 + clickHandler(event){
14 + console.log(event)
15 }
16
17 };
webui/components/settings/secrets/example-secrets.html renamed
+1 -1
@@ -26,7 +26,7 @@ BRAVE_API_KEY="brv_xxxxxxxxxxxxxxxxxxxxx"
26 GOOGLE_API_KEY="AIzaSyD-xxxxxxxxxxxxxxxxxxxx"
27
28 # Email password for notifications
29 -EMAIL_PASSWORD="another-secret-password"
29 +NOTIF_EMAIL_PASSWORD="another-secret-password"
30 `;
31
32 const editor = ace.edit("secrets-example");
webui/components/settings/secrets/example-vars.html new
+56
@@ -0,0 +1,56 @@
1 +<html>
2 +
3 +<head>
4 + <title>Example secrets file</title>
5 +
6 +</head>
7 +
8 +<body>
9 + <div x-data>
10 + <p>You can store passwords and secrets in standard <code>.env</code> format, one per line.<br>
11 +Add comments using <code>#</code> to help the agent understand the purpose of each secret.<br>
12 +See example below.</p>
13 +
14 +
15 + <h3>Example secrets file</h3>
16 + <div id="secrets-example"></div>
17 +
18 + <script>
19 + setTimeout(() => {
20 + const envExample = `# Sales email connection
21 +SALES_EMAIL_IMAP_HOST="imap.company.com"
22 +SALES_EMAIL_IMAP_PORT=993
23 +SALES_EMAIL_IMAP_SSL=true
24 +
25 +# Other email params
26 +CHECK_INTERVAL=60 # check every X seconds
27 +RETRY_INTERVAL=10 # retry every X seconds
28 +MAX_RETRIES=3 # max retries
29 +`;
30 +
31 + const editor = ace.edit("secrets-example");
32 + const dark = localStorage.getItem("darkMode");
33 + if (dark != "false") {
34 + editor.setTheme("ace/theme/github_dark");
35 + } else {
36 + editor.setTheme("ace/theme/tomorrow");
37 + }
38 + editor.session.setMode("ace/mode/ini");
39 + editor.setValue(envExample);
40 + editor.clearSelection();
41 + editor.setReadOnly(true);
42 + }, 0);
43 + </script>
44 + <!-- </template> -->
45 + </div>
46 +
47 + <style>
48 + #secrets-example {
49 + width: 100%;
50 + height: 20em;
51 + }
52 + </style>
53 +
54 +</body>
55 +
56 +</html>
\ No newline at end of file
webui/index.js
+16 -2
@@ -595,7 +595,7 @@ globalThis.resetChat = async function (ctxid = null) {
595
596 globalThis.newChat = async function () {
597 try {
598 - setContext(generateGUID());
598 + newContext();
599 updateAfterScroll();
600 } catch (e) {
601 globalThis.toastFetchError("Error creating new chat", e);
@@ -661,7 +661,7 @@ export function switchFromContext(id) {
661 setContext(alternateChat.id);
662 } else {
663 // If no other chats, create a new empty context
664 - setContext(generateGUID());
664 + newContext();
665 }
666 }
667 }
@@ -736,6 +736,20 @@ globalThis.selectChat = async function (id) {
736 updateAfterScroll();
737 };
738
739 +function generateShortId() {
740 + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
741 + let result = '';
742 + for (let i = 0; i < 8; i++) {
743 + result += chars.charAt(Math.floor(Math.random() * chars.length));
744 + }
745 + return result;
746 +}
747 +
748 +export const newContext = function () {
749 + context = generateShortId();
750 + setContext(context);
751 +}
752 +
753 export const setContext = function (id) {
754 if (id == context) return;
755 context = id;
webui/js/manifest.json
+1 -1
@@ -1,7 +1,7 @@
1 {
2 "name": "Agent Zero",
3 "short_name": "Agent Zero",
4 - "description": "The Resilient Autonomous Orchestrator",
4 + "description": "Autonomous AI agent",
5 "start_url": "/",
6 "display": "standalone",
7 "background_color": "#1a1a1a",