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",