GraziePrego commited on
Commit
cfa4e05
·
verified ·
1 Parent(s): a66110a

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +3 -50
  2. .gitattributes +3 -13
  3. .github/FUNDING.yml +3 -1
  4. .github/scripts/docker_release_plan.py +3 -841
  5. .github/workflows/close-inactive.yml +3 -108
  6. .github/workflows/docker-publish.yml +3 -236
  7. .gitignore +3 -52
  8. .vscode/extensions.json +3 -7
  9. .vscode/launch.json +3 -24
  10. .vscode/settings.json +3 -17
  11. AGENTS.md +3 -253
  12. DockerfileLocal +3 -36
  13. LICENSE +3 -23
  14. agent.py +3 -1023
  15. agents/_example/extensions/agent_init/_10_example_extension.py +3 -10
  16. agents/_example/prompts/agent.system.main.role.md +3 -8
  17. agents/_example/prompts/agent.system.tool.example_tool.md +3 -16
  18. agents/_example/tools/example_tool.py +3 -21
  19. agents/_example/tools/response.py +3 -23
  20. agents/a0_small/agent.yaml +3 -3
  21. agents/a0_small/prompts/agent.system.main.communication.md +3 -21
  22. agents/a0_small/prompts/agent.system.main.communication_additions.md +3 -10
  23. agents/a0_small/prompts/agent.system.main.role.md +3 -5
  24. agents/a0_small/prompts/agent.system.main.solving.md +3 -10
  25. agents/a0_small/prompts/agent.system.main.tips.md +3 -7
  26. agents/a0_small/prompts/agent.system.projects.active.md +3 -10
  27. agents/a0_small/prompts/agent.system.projects.inactive.md +3 -1
  28. agents/a0_small/prompts/agent.system.projects.main.md +3 -1
  29. agents/a0_small/prompts/agent.system.promptinclude.md +3 -6
  30. agents/a0_small/prompts/agent.system.response_tool_tips.md +3 -1
  31. agents/a0_small/prompts/agent.system.secrets.md +3 -10
  32. agents/a0_small/prompts/agent.system.skills.md +3 -3
  33. agents/a0_small/prompts/agent.system.tool.a2a_chat.md +3 -4
  34. agents/a0_small/prompts/agent.system.tool.behaviour.md +3 -4
  35. agents/a0_small/prompts/agent.system.tool.browser.md +3 -7
  36. agents/a0_small/prompts/agent.system.tool.call_sub.md +3 -11
  37. agents/a0_small/prompts/agent.system.tool.call_sub.py +3 -24
  38. agents/a0_small/prompts/agent.system.tool.code_exe.md +3 -12
  39. agents/a0_small/prompts/agent.system.tool.document_query.md +3 -8
  40. agents/a0_small/prompts/agent.system.tool.input.md +3 -4
  41. agents/a0_small/prompts/agent.system.tool.memory.md +3 -10
  42. agents/a0_small/prompts/agent.system.tool.notify_user.md +3 -5
  43. agents/a0_small/prompts/agent.system.tool.response.md +3 -5
  44. agents/a0_small/prompts/agent.system.tool.scheduler.md +3 -16
  45. agents/a0_small/prompts/agent.system.tool.search_engine.md +3 -5
  46. agents/a0_small/prompts/agent.system.tool.skills.md +3 -6
  47. agents/a0_small/prompts/agent.system.tool.text_editor.md +3 -11
  48. agents/a0_small/prompts/agent.system.tool.wait.md +3 -4
  49. agents/a0_small/prompts/agent.system.tools.md +3 -3
  50. agents/a0_small/prompts/agent.system.tools_vision.md +3 -4
.dockerignore CHANGED
@@ -1,50 +1,3 @@
1
- ###############################################################################
2
- # Project‑specific exclusions / re‑includes
3
- ###############################################################################
4
-
5
- # Obsolete
6
- memory/**
7
- instruments/**
8
- knowledge/custom/**
9
-
10
- # Logs, tmp, usr
11
- logs/*
12
- tmp/*
13
- usr/*
14
-
15
-
16
- # Keep .gitkeep markers anywhere
17
- !**/.gitkeep
18
-
19
-
20
- ###############################################################################
21
- # Environment / tooling
22
- ###############################################################################
23
- .conda/
24
- .cursor/
25
- .venv/
26
- .git/
27
-
28
-
29
- ###############################################################################
30
- # Tests (root‑level only)
31
- ###############################################################################
32
- /*.test.py
33
-
34
-
35
- ###############################################################################
36
- # ─── LAST SECTION: universal junk / caches (MUST BE LAST) ───
37
- # Put these at the *bottom* so they override any ! re‑includes above
38
- ###############################################################################
39
- # OS / editor junk
40
- **/.DS_Store
41
- **/Thumbs.db
42
-
43
- # Python caches / compiled artefacts
44
- **/__pycache__/
45
- **/*.py[cod]
46
- **/*.pyo
47
- **/*.pyd
48
-
49
- # Environment files anywhere
50
- *.env
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0e6fd1da71723dae7138d091caa8d50cb5c771b06cdebd332f199d109671e2f
3
+ size 1243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitattributes CHANGED
@@ -1,13 +1,3 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto eol=lfdocs/res/a0-vector-graphics/a0LogoVector.ai filter=lfs diff=lfs merge=lfs -text
3
- docs/res/dev/devinst-10.png filter=lfs diff=lfs merge=lfs -text
4
- docs/res/dev/devinst-2.png filter=lfs diff=lfs merge=lfs -text
5
- docs/res/devguide_vid.png filter=lfs diff=lfs merge=lfs -text
6
- docs/res/easy_ins_vid.png filter=lfs diff=lfs merge=lfs -text
7
- docs/res/setup/image-19.png filter=lfs diff=lfs merge=lfs -text
8
- docs/res/setup/thumb_play.png filter=lfs diff=lfs merge=lfs -text
9
- docs/res/time_example.jpg filter=lfs diff=lfs merge=lfs -text
10
- docs/res/usage/plugins/plugin-hub-main-view.png filter=lfs diff=lfs merge=lfs -text
11
- docs/res/usage/plugins/plugin-hub-plugin-detail.png filter=lfs diff=lfs merge=lfs -text
12
- docs/res/usage/plugins/plugins-list-01.png filter=lfs diff=lfs merge=lfs -text
13
- webui/vendor/google/google-icons.ttf filter=lfs diff=lfs merge=lfs -text
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:56c89d9018842070bb8dd1397378f26b814d30f4f7c56d8ff8788dbae1f83682
3
+ size 72
 
 
 
 
 
 
 
 
 
 
.github/FUNDING.yml CHANGED
@@ -1 +1,3 @@
1
- github: agent0ai
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fa60c383aeaebb5eb3071caaf9a568c174f8ad2f966bda74010417012e27ef2
3
+ size 17
.github/scripts/docker_release_plan.py CHANGED
@@ -1,841 +1,3 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import json
5
- import os
6
- import re
7
- import subprocess
8
- import sys
9
- from dataclasses import asdict, dataclass
10
- from pathlib import Path
11
- from urllib.error import HTTPError, URLError
12
- from urllib.parse import urlencode
13
- from urllib.request import Request, urlopen
14
-
15
-
16
- REPO_ROOT = Path(__file__).resolve().parents[2]
17
- OPENROUTER_CHAT_COMPLETIONS_URL = "https://openrouter.ai/api/v1/chat/completions"
18
- OPENROUTER_SYSTEM_PROMPT_PATH = REPO_ROOT / "scripts" / "openrouter_release_notes_system_prompt.md"
19
-
20
-
21
- def fail(message: str) -> None:
22
- print(message, file=sys.stderr)
23
- raise SystemExit(1)
24
-
25
-
26
- def write_output(name: str, value: str) -> None:
27
- output_path = os.environ.get("GITHUB_OUTPUT")
28
- if not output_path:
29
- return
30
- with open(output_path, "a", encoding="utf-8") as handle:
31
- handle.write(f"{name}<<__EOF__\n{value}\n__EOF__\n")
32
-
33
-
34
- def write_summary(lines: list[str]) -> None:
35
- summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
36
- if not summary_path or not lines:
37
- return
38
- with open(summary_path, "a", encoding="utf-8") as handle:
39
- handle.write("## Docker publish plan\n\n")
40
- for line in lines:
41
- handle.write(f"- {line}\n")
42
-
43
-
44
- def run_command(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
45
- result = subprocess.run(args, capture_output=True, text=True)
46
- if check and result.returncode != 0:
47
- command = " ".join(args)
48
- fail(f"Command failed ({command}):\n{result.stderr.strip()}")
49
- return result
50
-
51
-
52
- def git(*args: str, check: bool = True) -> str:
53
- return run_command("git", *args, check=check).stdout.strip()
54
-
55
-
56
- def docker_tag_exists(image_repo: str, tag: str) -> bool:
57
- result = run_command(
58
- "docker",
59
- "buildx",
60
- "imagetools",
61
- "inspect",
62
- f"{image_repo}:{tag}",
63
- check=False,
64
- )
65
- return result.returncode == 0
66
-
67
-
68
- def split_branches(raw: str) -> list[str]:
69
- parts = re.split(r"[\s,]+", raw.strip())
70
- return [part for part in parts if part]
71
-
72
-
73
- def require_env(name: str) -> str:
74
- value = os.environ.get(name, "").strip()
75
- if not value:
76
- fail(f"Required environment variable `{name}` is missing.")
77
- return value
78
-
79
-
80
- def require_any_env(*names: str) -> str:
81
- for name in names:
82
- value = os.environ.get(name, "").strip()
83
- if value:
84
- return value
85
- fail(
86
- "Required environment variable is missing. Expected one of: "
87
- + ", ".join(f"`{name}`" for name in names)
88
- )
89
-
90
-
91
- @dataclass(frozen=True)
92
- class Config:
93
- allowed_branches: list[str]
94
- main_branch: str
95
- image_repo: str
96
- tag_pattern: re.Pattern[str]
97
- min_version: tuple[int, int]
98
- event_name: str
99
- source_ref_name: str
100
- source_ref_type: str
101
- manual_tag: str
102
- before_sha: str
103
- after_sha: str
104
-
105
-
106
- @dataclass(frozen=True)
107
- class BranchState:
108
- branch: str
109
- valid_tags: list[str]
110
- latest_tag: str | None
111
-
112
-
113
- @dataclass
114
- class Candidate:
115
- branch: str
116
- source_tag: str
117
- mode: str
118
- publish_version: bool
119
- publish_branch_tag: bool
120
- reason: str
121
-
122
-
123
- @dataclass(frozen=True)
124
- class CommitEntry:
125
- heading: str
126
- description: str
127
-
128
-
129
- def load_config() -> Config:
130
- allowed_branches = split_branches(os.environ["ALLOWED_BRANCHES"])
131
- if not allowed_branches:
132
- fail("ALLOWED_BRANCHES must not be empty.")
133
- main_branch = os.environ["MAIN_BRANCH"].strip()
134
- if main_branch not in allowed_branches:
135
- fail("MAIN_BRANCH must also be listed in ALLOWED_BRANCHES.")
136
-
137
- tag_regex = os.environ["RELEASE_TAG_REGEX"]
138
- return Config(
139
- allowed_branches=allowed_branches,
140
- main_branch=main_branch,
141
- image_repo=os.environ["DOCKER_IMAGE_REPO"].strip(),
142
- tag_pattern=re.compile(tag_regex),
143
- min_version=(
144
- int(os.environ["MIN_RELEASE_MAJOR"]),
145
- int(os.environ["MIN_RELEASE_MINOR"]),
146
- ),
147
- event_name=os.environ["EVENT_NAME"].strip(),
148
- source_ref_name=os.environ.get("SOURCE_REF_NAME", "").strip(),
149
- source_ref_type=os.environ.get("SOURCE_REF_TYPE", "").strip(),
150
- manual_tag=os.environ.get("MANUAL_TAG", "").strip(),
151
- before_sha=os.environ.get("BEFORE_SHA", "").strip(),
152
- after_sha=os.environ.get("AFTER_SHA", "").strip(),
153
- )
154
-
155
-
156
- def parse_release_tag(config: Config, tag: str) -> tuple[int, int] | None:
157
- match = config.tag_pattern.fullmatch(tag)
158
- if not match:
159
- return None
160
- version = (int(match.group(1)), int(match.group(2)))
161
- if version < config.min_version:
162
- return None
163
- return version
164
-
165
-
166
- def tag_exists(tag: str) -> bool:
167
- return run_command("git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}", check=False).returncode == 0
168
-
169
-
170
- def tag_commit(tag: str) -> str:
171
- return git("rev-list", "-n", "1", f"refs/tags/{tag}")
172
-
173
-
174
- def commit_is_ancestor(older_ref: str, newer_ref: str) -> bool:
175
- return (
176
- run_command(
177
- "git",
178
- "merge-base",
179
- "--is-ancestor",
180
- older_ref,
181
- newer_ref,
182
- check=False,
183
- ).returncode
184
- == 0
185
- )
186
-
187
-
188
- def branch_contains_commit(branch: str, commit: str) -> bool:
189
- return (
190
- run_command(
191
- "git",
192
- "merge-base",
193
- "--is-ancestor",
194
- commit,
195
- f"origin/{branch}",
196
- check=False,
197
- ).returncode
198
- == 0
199
- )
200
-
201
-
202
- def ref_exists(ref: str) -> bool:
203
- if not ref or re.fullmatch(r"0{40}", ref):
204
- return False
205
- return run_command("git", "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}", check=False).returncode == 0
206
-
207
-
208
- def releasable_tags_for_ref(config: Config, ref: str) -> list[str]:
209
- if not ref_exists(ref):
210
- return []
211
-
212
- tagged_versions: list[tuple[tuple[int, int], str]] = []
213
- merged_tags = git("tag", "--merged", ref)
214
- for tag in merged_tags.splitlines():
215
- version = parse_release_tag(config, tag.strip())
216
- if version is None:
217
- continue
218
- tagged_versions.append((version, tag.strip()))
219
-
220
- tagged_versions.sort(key=lambda item: item[0])
221
- return [tag for _, tag in tagged_versions]
222
-
223
-
224
- def latest_releasable_tag_for_ref(config: Config, ref: str) -> str | None:
225
- valid_tags = releasable_tags_for_ref(config, ref)
226
- return valid_tags[-1] if valid_tags else None
227
-
228
-
229
- def collect_branch_states(config: Config, branches: list[str] | None = None) -> dict[str, BranchState]:
230
- states: dict[str, BranchState] = {}
231
- for branch in branches or config.allowed_branches:
232
- if run_command("git", "show-ref", "--verify", "--quiet", f"refs/remotes/origin/{branch}", check=False).returncode != 0:
233
- fail(f"Allowed branch origin/{branch} was not fetched.")
234
-
235
- valid_tags = releasable_tags_for_ref(config, f"origin/{branch}")
236
- states[branch] = BranchState(
237
- branch=branch,
238
- valid_tags=valid_tags,
239
- latest_tag=valid_tags[-1] if valid_tags else None,
240
- )
241
- return states
242
-
243
-
244
- def add_or_merge_candidate(candidates: dict[tuple[str, str, str], Candidate], candidate: Candidate) -> None:
245
- key = (candidate.branch, candidate.source_tag, candidate.mode)
246
- existing = candidates.get(key)
247
- if existing is None:
248
- candidates[key] = candidate
249
- return
250
- existing.publish_version = existing.publish_version or candidate.publish_version
251
- existing.publish_branch_tag = existing.publish_branch_tag or candidate.publish_branch_tag
252
- if candidate.reason not in existing.reason:
253
- existing.reason = f"{existing.reason}; {candidate.reason}"
254
-
255
-
256
- def plan_tag_push(config: Config, branch_states: dict[str, BranchState]) -> tuple[list[Candidate], list[str]]:
257
- source_tag = config.source_ref_name
258
- notes: list[str] = []
259
- version = parse_release_tag(config, source_tag)
260
- if version is None:
261
- return [], [f"Skipped `{source_tag}` because it does not match `v{{X}}.{{Y}}` or is below v{config.min_version[0]}.{config.min_version[1]}."]
262
- if not tag_exists(source_tag):
263
- return [], [f"Skipped `{source_tag}` because the tag is not present after checkout."]
264
-
265
- commit = tag_commit(source_tag)
266
- candidates: list[Candidate] = []
267
- found_branch = False
268
- for branch, state in branch_states.items():
269
- if not branch_contains_commit(branch, commit):
270
- continue
271
- found_branch = True
272
- if state.latest_tag != source_tag:
273
- notes.append(
274
- f"Skipped `{source_tag}` on `{branch}` because `{state.latest_tag}` is the highest release tag currently reachable from that branch."
275
- )
276
- continue
277
- candidates.append(
278
- Candidate(
279
- branch=branch,
280
- source_tag=source_tag,
281
- mode="push_latest_only",
282
- publish_version=branch == config.main_branch,
283
- publish_branch_tag=True,
284
- reason=f"Automatic build for the latest release tag on `{branch}`.",
285
- )
286
- )
287
-
288
- if not found_branch:
289
- notes.append(f"Skipped `{source_tag}` because it is not reachable from any allowed branch.")
290
- return candidates, notes
291
-
292
-
293
- def plan_branch_push(config: Config, branch_states: dict[str, BranchState]) -> tuple[list[Candidate], list[str]]:
294
- branch = config.source_ref_name
295
- if branch not in branch_states:
296
- return [], [f"Skipped `{branch}` because it is not an allowed release branch."]
297
-
298
- before_tag = latest_releasable_tag_for_ref(config, config.before_sha)
299
- after_tag = branch_states[branch].latest_tag
300
- if after_tag is None:
301
- return [], [f"Skipped `{branch}` because it has no releasable tags."]
302
- if before_tag == after_tag:
303
- return [], [f"Skipped `{branch}` because its highest release tag is still `{after_tag}`."]
304
-
305
- return [
306
- Candidate(
307
- branch=branch,
308
- source_tag=after_tag,
309
- mode="push_promoted_tag",
310
- publish_version=branch == config.main_branch,
311
- publish_branch_tag=True,
312
- reason=f"Automatic build for `{after_tag}` after it reached `{branch}`.",
313
- )
314
- ], []
315
-
316
-
317
- def plan_manual_exact(config: Config, branch_states: dict[str, BranchState]) -> tuple[list[Candidate], list[str]]:
318
- manual_tag = config.manual_tag
319
- if parse_release_tag(config, manual_tag) is None:
320
- fail(
321
- f"Manual tag `{manual_tag}` is invalid. Expected `v{{X}}.{{Y}}` with a minimum of v{config.min_version[0]}.{config.min_version[1]}."
322
- )
323
- if not tag_exists(manual_tag):
324
- fail(f"Manual tag `{manual_tag}` does not exist in the repository.")
325
-
326
- commit = tag_commit(manual_tag)
327
- notes: list[str] = []
328
- candidates: list[Candidate] = []
329
- for branch, state in branch_states.items():
330
- if not branch_contains_commit(branch, commit):
331
- continue
332
- if branch == config.main_branch:
333
- candidates.append(
334
- Candidate(
335
- branch=branch,
336
- source_tag=manual_tag,
337
- mode="manual_exact",
338
- publish_version=True,
339
- publish_branch_tag=state.latest_tag == manual_tag,
340
- reason=f"Manual rebuild for `{manual_tag}` on `{branch}`.",
341
- )
342
- )
343
- continue
344
- if state.latest_tag != manual_tag:
345
- notes.append(
346
- f"Skipped `{manual_tag}` on `{branch}` because non-main branches only publish their current branch tag and `{state.latest_tag}` is newer."
347
- )
348
- continue
349
- candidates.append(
350
- Candidate(
351
- branch=branch,
352
- source_tag=manual_tag,
353
- mode="manual_exact",
354
- publish_version=False,
355
- publish_branch_tag=True,
356
- reason=f"Manual rebuild for the current branch image on `{branch}`.",
357
- )
358
- )
359
-
360
- if not candidates:
361
- notes.append(f"No eligible images were found for manual tag `{manual_tag}`.")
362
- return candidates, notes
363
-
364
-
365
- def plan_manual_backfill(config: Config, branch_states: dict[str, BranchState]) -> tuple[list[Candidate], list[str]]:
366
- notes: list[str] = []
367
- candidates: dict[tuple[str, str, str], Candidate] = {}
368
-
369
- for branch, state in branch_states.items():
370
- if not state.valid_tags:
371
- notes.append(f"Branch `{branch}` has no releasable tags.")
372
- continue
373
-
374
- if branch == config.main_branch:
375
- for tag in state.valid_tags:
376
- if docker_tag_exists(config.image_repo, tag):
377
- continue
378
- add_or_merge_candidate(
379
- candidates,
380
- Candidate(
381
- branch=branch,
382
- source_tag=tag,
383
- mode="manual_backfill",
384
- publish_version=True,
385
- publish_branch_tag=False,
386
- reason=f"Missing Docker Hub tag `{tag}`.",
387
- ),
388
- )
389
-
390
- latest_tag = state.latest_tag
391
- if latest_tag and not docker_tag_exists(config.image_repo, "latest"):
392
- add_or_merge_candidate(
393
- candidates,
394
- Candidate(
395
- branch=branch,
396
- source_tag=latest_tag,
397
- mode="manual_backfill",
398
- publish_version=False,
399
- publish_branch_tag=True,
400
- reason="Missing Docker Hub tag `latest`.",
401
- ),
402
- )
403
- continue
404
-
405
- if not docker_tag_exists(config.image_repo, branch):
406
- add_or_merge_candidate(
407
- candidates,
408
- Candidate(
409
- branch=branch,
410
- source_tag=state.latest_tag,
411
- mode="manual_backfill",
412
- publish_version=False,
413
- publish_branch_tag=True,
414
- reason=f"Missing Docker Hub tag `{branch}`.",
415
- ),
416
- )
417
-
418
- if not candidates:
419
- notes.append("No missing Docker Hub tags were found.")
420
- return list(candidates.values()), notes
421
-
422
-
423
- def plan_command() -> None:
424
- config = load_config()
425
- branch_states = collect_branch_states(config)
426
-
427
- if config.event_name == "workflow_dispatch":
428
- if config.manual_tag:
429
- candidates, notes = plan_manual_exact(config, branch_states)
430
- else:
431
- candidates, notes = plan_manual_backfill(config, branch_states)
432
- elif config.event_name == "push":
433
- if config.source_ref_type == "tag":
434
- candidates, notes = plan_tag_push(config, branch_states)
435
- elif config.source_ref_type == "branch":
436
- candidates, notes = plan_branch_push(config, branch_states)
437
- else:
438
- fail(f"Unsupported push ref type: {config.source_ref_type}")
439
- else:
440
- fail(f"Unsupported event: {config.event_name}")
441
-
442
- summary_lines = [candidate.reason for candidate in candidates]
443
- summary_lines.extend(notes)
444
-
445
- matrix = {"include": [asdict(candidate) for candidate in candidates]}
446
- write_output("has_work", "true" if candidates else "false")
447
- write_output("matrix", json.dumps(matrix))
448
- write_summary(summary_lines)
449
-
450
- print(json.dumps(matrix, indent=2))
451
- for line in summary_lines:
452
- print(f"- {line}")
453
-
454
-
455
- def unique(items: list[str]) -> list[str]:
456
- seen: set[str] = set()
457
- output: list[str] = []
458
- for item in items:
459
- if item in seen:
460
- continue
461
- seen.add(item)
462
- output.append(item)
463
- return output
464
-
465
-
466
- def load_text(path: Path) -> str:
467
- if not path.exists():
468
- fail(f"Expected file `{path}` to exist.")
469
- return path.read_text(encoding="utf-8").strip()
470
-
471
-
472
- def github_repository_parts() -> tuple[str, str]:
473
- repository = require_env("GITHUB_REPOSITORY")
474
- owner, separator, repo = repository.partition("/")
475
- if not owner or not separator or not repo:
476
- fail(f"GITHUB_REPOSITORY must be in `owner/repo` format, got `{repository}`.")
477
- return owner, repo
478
-
479
-
480
- def github_api_get(path: str, params: dict[str, str | int] | None = None) -> object:
481
- api_base = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/")
482
- token = require_env("GITHUB_TOKEN")
483
- query = f"?{urlencode(params)}" if params else ""
484
- request = Request(
485
- f"{api_base}{path}{query}",
486
- headers={
487
- "Accept": "application/vnd.github+json",
488
- "Authorization": f"Bearer {token}",
489
- "X-GitHub-Api-Version": "2022-11-28",
490
- },
491
- method="GET",
492
- )
493
-
494
- try:
495
- with urlopen(request, timeout=30) as response:
496
- return json.loads(response.read().decode("utf-8"))
497
- except HTTPError as exc:
498
- details = exc.read().decode("utf-8", errors="replace").strip()
499
- fail(f"GitHub API request failed ({path}): {exc.code} {exc.reason}\n{details}")
500
- except URLError as exc:
501
- fail(f"GitHub API request failed ({path}): {exc.reason}")
502
-
503
-
504
- def list_github_releases() -> list[dict[str, object]]:
505
- owner, repo = github_repository_parts()
506
- releases: list[dict[str, object]] = []
507
- page = 1
508
-
509
- while True:
510
- payload = github_api_get(
511
- f"/repos/{owner}/{repo}/releases",
512
- {"per_page": 100, "page": page},
513
- )
514
- if not isinstance(payload, list):
515
- fail("GitHub releases response was not a list.")
516
- page_items = [item for item in payload if isinstance(item, dict)]
517
- releases.extend(page_items)
518
- if len(page_items) < 100:
519
- break
520
- page += 1
521
-
522
- return releases
523
-
524
-
525
- def previous_published_release_tag(config: Config, source_tag: str) -> str | None:
526
- source_version = parse_release_tag(config, source_tag)
527
- if source_version is None:
528
- fail(f"Tag `{source_tag}` is not a releasable tag.")
529
-
530
- previous: list[tuple[tuple[int, int], str]] = []
531
- for release in list_github_releases():
532
- if release.get("draft") or release.get("prerelease"):
533
- continue
534
- tag_name = str(release.get("tag_name", "")).strip()
535
- version = parse_release_tag(config, tag_name)
536
- if version is None or version >= source_version:
537
- continue
538
- previous.append((version, tag_name))
539
-
540
- previous.sort(key=lambda item: item[0])
541
- return previous[-1][1] if previous else None
542
-
543
-
544
- def parse_commit_entries(raw_log: str) -> list[CommitEntry]:
545
- entries: list[CommitEntry] = []
546
- for raw_entry in raw_log.split("\x1e"):
547
- entry = raw_entry.strip()
548
- if not entry:
549
- continue
550
- heading, separator, description = entry.partition("\x1f")
551
- if not separator:
552
- continue
553
- entries.append(
554
- CommitEntry(
555
- heading=re.sub(r"\s+", " ", heading).strip(),
556
- description=description.strip(),
557
- )
558
- )
559
- return entries
560
-
561
-
562
- def collect_release_commits(previous_release_tag: str | None, source_tag: str) -> list[CommitEntry]:
563
- range_ref = source_tag
564
- if previous_release_tag:
565
- if not tag_exists(previous_release_tag):
566
- fail(f"Previous published release tag `{previous_release_tag}` is not available in the repository.")
567
- if not commit_is_ancestor(
568
- f"refs/tags/{previous_release_tag}^{{commit}}",
569
- f"refs/tags/{source_tag}^{{commit}}",
570
- ):
571
- fail(
572
- f"Previous published release tag `{previous_release_tag}` is not an ancestor of `{source_tag}`."
573
- )
574
- range_ref = f"{previous_release_tag}..{source_tag}"
575
-
576
- raw_log = git("log", "--reverse", "--format=%s%x1f%b%x1e", range_ref)
577
- return parse_commit_entries(raw_log)
578
-
579
-
580
- def build_release_notes_user_message(commits: list[CommitEntry]) -> str:
581
- lines = ["Commit headings and descriptions:"]
582
-
583
- if not commits:
584
- lines.append("No commits were found in this release range.")
585
- return "\n".join(lines)
586
-
587
- for index, commit in enumerate(commits, start=1):
588
- lines.append(f"{index}. Heading: {commit.heading}")
589
- if commit.description:
590
- lines.append("Description:")
591
- lines.append(commit.description)
592
- else:
593
- lines.append("Description: (none)")
594
- lines.append("")
595
-
596
- return "\n".join(lines).strip()
597
-
598
-
599
- def extract_openrouter_message_content(payload: object) -> str:
600
- if not isinstance(payload, dict):
601
- return ""
602
-
603
- content = payload.get("content")
604
- if isinstance(content, str):
605
- return content
606
- if not isinstance(content, list):
607
- return ""
608
-
609
- parts: list[str] = []
610
- for part in content:
611
- if not isinstance(part, dict):
612
- continue
613
- text = part.get("text")
614
- if isinstance(text, str):
615
- parts.append(text)
616
- return "\n".join(parts)
617
-
618
-
619
- def generate_release_body_with_openrouter(commits: list[CommitEntry]) -> str:
620
- api_key = require_env("OPENROUTER_API_KEY")
621
- model = require_any_env("OPENROUTER_MODEL_NAME", "OPENROUTER_MODEL")
622
- system_prompt = load_text(OPENROUTER_SYSTEM_PROMPT_PATH)
623
- repository = require_env("GITHUB_REPOSITORY")
624
- user_message = build_release_notes_user_message(commits)
625
-
626
- payload = {
627
- "model": model,
628
- "messages": [
629
- {"role": "system", "content": system_prompt},
630
- {"role": "user", "content": user_message},
631
- ],
632
- "temperature": 0.2,
633
- }
634
- request = Request(
635
- OPENROUTER_CHAT_COMPLETIONS_URL,
636
- data=json.dumps(payload).encode("utf-8"),
637
- headers={
638
- "Authorization": f"Bearer {api_key}",
639
- "Content-Type": "application/json",
640
- "HTTP-Referer": f"https://github.com/{repository}",
641
- "X-OpenRouter-Title": "Agent Zero Docker Release Notes",
642
- },
643
- method="POST",
644
- )
645
-
646
- try:
647
- with urlopen(request, timeout=60) as response:
648
- response_payload = json.loads(response.read().decode("utf-8"))
649
- except HTTPError as exc:
650
- details = exc.read().decode("utf-8", errors="replace").strip()
651
- fail(f"OpenRouter request failed: {exc.code} {exc.reason}\n{details}")
652
- except URLError as exc:
653
- fail(f"OpenRouter request failed: {exc.reason}")
654
-
655
- if not isinstance(response_payload, dict):
656
- fail("OpenRouter response was not a JSON object.")
657
-
658
- choices = response_payload.get("choices")
659
- if not isinstance(choices, list) or not choices:
660
- fail(f"OpenRouter response did not include choices: {json.dumps(response_payload)}")
661
-
662
- first_choice = choices[0]
663
- if not isinstance(first_choice, dict):
664
- fail("OpenRouter returned an invalid choice payload.")
665
-
666
- message = first_choice.get("message")
667
- body = extract_openrouter_message_content(message).strip()
668
- return body or "No release notes."
669
-
670
-
671
- def resolve_release_command() -> None:
672
- config = load_config()
673
- branch = os.environ["TARGET_BRANCH"].strip()
674
- source_tag = os.environ["TARGET_TAG"].strip()
675
-
676
- if branch != config.main_branch:
677
- write_output("should_release", "false")
678
- write_output("skip_reason", f"Branch `{branch}` does not publish GitHub releases.")
679
- return
680
-
681
- branch_state = collect_branch_states(config, [branch])[branch]
682
- if branch_state.latest_tag is None:
683
- write_output("should_release", "false")
684
- write_output("skip_reason", f"Branch `{branch}` has no releasable tags.")
685
- return
686
-
687
- if parse_release_tag(config, source_tag) is None or not tag_exists(source_tag):
688
- write_output("should_release", "false")
689
- write_output("skip_reason", f"Tag `{source_tag}` is not a releasable tag.")
690
- return
691
-
692
- commit = tag_commit(source_tag)
693
- if not branch_contains_commit(branch, commit):
694
- write_output("should_release", "false")
695
- write_output("skip_reason", f"Tag `{source_tag}` is no longer reachable from `{branch}`.")
696
- return
697
-
698
- if branch_state.latest_tag != source_tag:
699
- write_output("should_release", "false")
700
- write_output(
701
- "skip_reason",
702
- f"Tag `{source_tag}` is not the highest release tag on `{branch}`.",
703
- )
704
- return
705
-
706
- previous_release_tag = ""
707
- commits: list[CommitEntry] = []
708
- body = "Failed to generate release notes."
709
- try:
710
- previous_release_tag = previous_published_release_tag(config, source_tag) or ""
711
- commits = collect_release_commits(previous_release_tag or None, source_tag)
712
- body = generate_release_body_with_openrouter(commits)
713
- except SystemExit:
714
- print(
715
- f"Release note generation failed for `{source_tag}`. Falling back to a static release body.",
716
- file=sys.stderr,
717
- )
718
- except Exception as exc:
719
- print(
720
- f"Unexpected release note generation error for `{source_tag}`: {exc}. Falling back to a static release body.",
721
- file=sys.stderr,
722
- )
723
-
724
- write_output("should_release", "true")
725
- write_output("release_tag", source_tag)
726
- write_output("release_name", source_tag)
727
- write_output("previous_release_tag", previous_release_tag)
728
- write_output("release_commit_count", str(len(commits)))
729
- write_output("release_body", body)
730
- print(source_tag)
731
-
732
-
733
- def resolve_build_command() -> None:
734
- config = load_config()
735
- branch = os.environ["TARGET_BRANCH"].strip()
736
- source_tag = os.environ["TARGET_TAG"].strip()
737
- mode = os.environ["TARGET_MODE"].strip()
738
- publish_version = os.environ["TARGET_PUBLISH_VERSION"].strip().lower() == "true"
739
- publish_branch_tag = os.environ["TARGET_PUBLISH_BRANCH_TAG"].strip().lower() == "true"
740
-
741
- branch_state = collect_branch_states(config, [branch])[branch]
742
- if branch_state.latest_tag is None:
743
- write_output("should_build", "false")
744
- write_output("skip_reason", f"Branch `{branch}` has no releasable tags.")
745
- return
746
-
747
- if parse_release_tag(config, source_tag) is None or not tag_exists(source_tag):
748
- write_output("should_build", "false")
749
- write_output("skip_reason", f"Tag `{source_tag}` is no longer available.")
750
- return
751
-
752
- commit = tag_commit(source_tag)
753
- if not branch_contains_commit(branch, commit):
754
- write_output("should_build", "false")
755
- write_output("skip_reason", f"Tag `{source_tag}` is no longer reachable from `{branch}`.")
756
- return
757
-
758
- mutable_tag = "latest" if branch == config.main_branch else branch
759
- tags_to_push: list[str] = []
760
-
761
- if mode == "push_latest_only":
762
- if branch_state.latest_tag != source_tag:
763
- write_output("should_build", "false")
764
- write_output(
765
- "skip_reason",
766
- f"Tag `{source_tag}` is no longer the highest release tag on `{branch}`.",
767
- )
768
- return
769
- if publish_version:
770
- tags_to_push.append(f"{config.image_repo}:{source_tag}")
771
- if publish_branch_tag:
772
- tags_to_push.append(f"{config.image_repo}:{mutable_tag}")
773
-
774
- elif mode == "push_promoted_tag":
775
- if branch_state.latest_tag != source_tag:
776
- write_output("should_build", "false")
777
- write_output(
778
- "skip_reason",
779
- f"Tag `{source_tag}` is no longer the highest release tag on `{branch}`.",
780
- )
781
- return
782
- if publish_version and not docker_tag_exists(config.image_repo, source_tag):
783
- tags_to_push.append(f"{config.image_repo}:{source_tag}")
784
- if publish_branch_tag:
785
- tags_to_push.append(f"{config.image_repo}:{mutable_tag}")
786
-
787
- elif mode == "manual_exact":
788
- if publish_version:
789
- tags_to_push.append(f"{config.image_repo}:{source_tag}")
790
- if publish_branch_tag and branch_state.latest_tag == source_tag:
791
- tags_to_push.append(f"{config.image_repo}:{mutable_tag}")
792
-
793
- elif mode == "manual_backfill":
794
- if publish_version and not docker_tag_exists(config.image_repo, source_tag):
795
- tags_to_push.append(f"{config.image_repo}:{source_tag}")
796
- if publish_branch_tag:
797
- if branch != config.main_branch and branch_state.latest_tag != source_tag:
798
- write_output("should_build", "false")
799
- write_output(
800
- "skip_reason",
801
- f"Tag `{source_tag}` is no longer the newest release tag on `{branch}`.",
802
- )
803
- return
804
- if branch == config.main_branch and branch_state.latest_tag != source_tag:
805
- publish_branch_tag = False
806
- if publish_branch_tag and not docker_tag_exists(config.image_repo, mutable_tag):
807
- tags_to_push.append(f"{config.image_repo}:{mutable_tag}")
808
- else:
809
- fail(f"Unsupported resolve-build mode: {mode}")
810
-
811
- tags_to_push = unique(tags_to_push)
812
- if not tags_to_push:
813
- write_output("should_build", "false")
814
- write_output("skip_reason", "All requested Docker tags already exist or are no longer eligible.")
815
- return
816
-
817
- write_output("should_build", "true")
818
- write_output("tags", "\n".join(tags_to_push))
819
- write_output("display_tags", ", ".join(tag.rsplit(":", 1)[1] for tag in tags_to_push))
820
- print("\n".join(tags_to_push))
821
-
822
-
823
- def main() -> None:
824
- if len(sys.argv) != 2:
825
- fail("Usage: docker_release_plan.py <plan|resolve-build|resolve-release>")
826
-
827
- command = sys.argv[1]
828
- if command == "plan":
829
- plan_command()
830
- return
831
- if command == "resolve-build":
832
- resolve_build_command()
833
- return
834
- if command == "resolve-release":
835
- resolve_release_command()
836
- return
837
- fail(f"Unknown command: {command}")
838
-
839
-
840
- if __name__ == "__main__":
841
- main()
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:896b9a98df30ded250d8dcb95bac2b80184fccffaa5908ac2ef9c7a3d58a8442
3
+ size 29693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/close-inactive.yml CHANGED
@@ -1,108 +1,3 @@
1
- name: Close inactive issues and PRs
2
-
3
- on:
4
- schedule:
5
- - cron: "17 3 * * *"
6
- workflow_dispatch:
7
- inputs:
8
- inactive_days:
9
- description: "Close items with no activity for more than N days"
10
- required: false
11
- default: "90"
12
- dry_run:
13
- description: "If true, only print URLs (no comment/close)"
14
- required: false
15
- default: "true"
16
-
17
- permissions:
18
- issues: write
19
- pull-requests: write
20
-
21
- env:
22
- DEFAULT_INACTIVE_DAYS: "90"
23
- DEFAULT_DRY_RUN: "false"
24
-
25
- jobs:
26
- close_inactive:
27
- if: github.repository == 'agent0ai/agent-zero' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
28
- runs-on: ubuntu-latest
29
- steps:
30
- - name: Find and optionally close inactive issues/PRs
31
- uses: actions/github-script@v7
32
- env:
33
- INACTIVE_DAYS: ${{ github.event_name == 'workflow_dispatch' && inputs.inactive_days || env.DEFAULT_INACTIVE_DAYS }}
34
- DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || env.DEFAULT_DRY_RUN }}
35
- with:
36
- script: |
37
- const inactiveDaysRaw = process.env.INACTIVE_DAYS ?? "90";
38
- const inactiveDays = Number.parseInt(inactiveDaysRaw, 10);
39
- if (!Number.isFinite(inactiveDays) || inactiveDays <= 0) {
40
- core.setFailed(`Invalid INACTIVE_DAYS: ${inactiveDaysRaw}`);
41
- return;
42
- }
43
-
44
- const dryRunRaw = (process.env.DRY_RUN ?? "true").toLowerCase();
45
- const dryRun = ["1", "true", "yes", "y"].includes(dryRunRaw);
46
-
47
- const now = new Date();
48
- const cutoff = new Date(now.getTime() - inactiveDays * 24 * 60 * 60 * 1000);
49
- const cutoffDate = cutoff.toISOString().slice(0, 10);
50
-
51
- core.info(`inactiveDays=${inactiveDays}`);
52
- core.info(`dryRun=${dryRun}`);
53
- core.info(`cutoffDate=${cutoffDate}`);
54
-
55
- const owner = context.repo.owner;
56
- const repo = context.repo.repo;
57
-
58
- async function processQuery(kind, searchQuery) {
59
- core.info(`Searching ${kind}: ${searchQuery}`);
60
-
61
- const items = await github.paginate(github.rest.search.issuesAndPullRequests, {
62
- q: searchQuery,
63
- per_page: 100,
64
- });
65
-
66
- if (items.length === 0) {
67
- core.info(`No inactive ${kind} found.`);
68
- return;
69
- }
70
-
71
- core.info(`Found ${items.length} inactive ${kind}. URLs:`);
72
- for (const item of items) {
73
- core.info(item.html_url);
74
- }
75
-
76
- if (dryRun) {
77
- return;
78
- }
79
-
80
- for (const item of items) {
81
- const issueNumber = item.number;
82
- const url = item.html_url;
83
-
84
- try {
85
- await github.rest.issues.createComment({
86
- owner,
87
- repo,
88
- issue_number: issueNumber,
89
- body: `Closing due to inactivity of ${inactiveDays} days.`,
90
- });
91
-
92
- await github.rest.issues.update({
93
- owner,
94
- repo,
95
- issue_number: issueNumber,
96
- state: "closed",
97
- });
98
-
99
- core.info(`Closed: ${url}`);
100
- } catch (err) {
101
- core.warning(`Failed to close ${url}: ${err?.message ?? String(err)}`);
102
- }
103
- }
104
- }
105
-
106
- const base = `repo:${owner}/${repo} is:open updated:<${cutoffDate} sort:updated-asc`;
107
- await processQuery("issues", `${base} is:issue`);
108
- await processQuery("pull requests", `${base} is:pr`);
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02195272e30ac37e3732b583e3d25c9b24282c6999ebe28a01061a08b6c512cd
3
+ size 3740
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/docker-publish.yml CHANGED
@@ -1,236 +1,3 @@
1
- name: Build And Publish Docker Images
2
-
3
- on:
4
- push:
5
- branches:
6
- - "testing"
7
- - "ready"
8
- - "main"
9
- tags:
10
- - "v*"
11
- workflow_dispatch:
12
- inputs:
13
- tag:
14
- description: "Optional release tag to rebuild, for example v1.21"
15
- required: false
16
- type: string
17
-
18
- env:
19
- # Non-main branches publish a Docker tag with the same name as the branch.
20
- ALLOWED_BRANCHES: "testing ready main"
21
- MAIN_BRANCH: "main"
22
- RELEASE_TAG_REGEX: "^v([0-9]+)\\.([0-9]+)$"
23
- MIN_RELEASE_MAJOR: "1"
24
- MIN_RELEASE_MINOR: "0"
25
- DOCKERFILE_DIR: "docker/run"
26
- DOCKERFILE_PATH: "docker/run/Dockerfile"
27
- DOCKER_IMAGE_NAME: "agent-zero"
28
- DOCKER_PLATFORMS: "linux/amd64,linux/arm64"
29
-
30
- permissions:
31
- contents: read
32
-
33
- jobs:
34
- plan:
35
- if: github.repository == 'agent0ai/agent-zero'
36
- runs-on: ubuntu-latest
37
- outputs:
38
- has_work: ${{ steps.plan.outputs.has_work }}
39
- matrix: ${{ steps.plan.outputs.matrix }}
40
- steps:
41
- - name: Validate Docker Hub secrets
42
- env:
43
- DOCKERHUB_ORG: ${{ secrets.DOCKERHUB_ORG }}
44
- DOCKERHUB_OAT_TOKEN: ${{ secrets.DOCKERHUB_OAT_TOKEN }}
45
- run: |
46
- if [[ -z "$DOCKERHUB_ORG" || -z "$DOCKERHUB_OAT_TOKEN" ]]; then
47
- echo "::error::Missing DOCKERHUB_ORG or DOCKERHUB_OAT_TOKEN secret."
48
- exit 1
49
- fi
50
-
51
- - name: Check out repository
52
- uses: actions/checkout@v4
53
- with:
54
- fetch-depth: 0
55
-
56
- - name: Fetch remote branches and tags
57
- run: git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*'
58
-
59
- - name: Set up Docker Buildx
60
- uses: docker/setup-buildx-action@v3
61
-
62
- - name: Log in to Docker Hub
63
- uses: docker/login-action@v3
64
- with:
65
- username: ${{ secrets.DOCKERHUB_ORG }}
66
- password: ${{ secrets.DOCKERHUB_OAT_TOKEN }}
67
-
68
- - name: Plan Docker publish targets
69
- id: plan
70
- env:
71
- EVENT_NAME: ${{ github.event_name }}
72
- SOURCE_REF_NAME: ${{ github.ref_name }}
73
- SOURCE_REF_TYPE: ${{ github.ref_type }}
74
- BEFORE_SHA: ${{ github.event_name == 'push' && github.event.before || '' }}
75
- AFTER_SHA: ${{ github.event_name == 'push' && github.sha || '' }}
76
- MANUAL_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
77
- DOCKER_IMAGE_REPO: ${{ format('{0}/{1}', secrets.DOCKERHUB_ORG, env.DOCKER_IMAGE_NAME) }}
78
- run: python3 .github/scripts/docker_release_plan.py plan
79
-
80
- build:
81
- if: needs.plan.outputs.has_work == 'true'
82
- needs: plan
83
- runs-on: ubuntu-latest
84
- permissions:
85
- contents: write
86
- strategy:
87
- fail-fast: false
88
- matrix: ${{ fromJson(needs.plan.outputs.matrix) }}
89
- concurrency:
90
- group: docker-publish-${{ github.repository }}-${{ matrix.branch }}
91
- cancel-in-progress: false
92
- steps:
93
- - name: Check out repository
94
- uses: actions/checkout@v4
95
- with:
96
- fetch-depth: 0
97
-
98
- - name: Fetch remote branches and tags
99
- run: git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*'
100
-
101
- - name: Validate Docker Hub secrets
102
- env:
103
- DOCKERHUB_ORG: ${{ secrets.DOCKERHUB_ORG }}
104
- DOCKERHUB_OAT_TOKEN: ${{ secrets.DOCKERHUB_OAT_TOKEN }}
105
- run: |
106
- if [[ -z "$DOCKERHUB_ORG" || -z "$DOCKERHUB_OAT_TOKEN" ]]; then
107
- echo "::error::Missing DOCKERHUB_ORG or DOCKERHUB_OAT_TOKEN secret."
108
- exit 1
109
- fi
110
-
111
- - name: Set up QEMU
112
- uses: docker/setup-qemu-action@v3
113
-
114
- - name: Set up Docker Buildx
115
- uses: docker/setup-buildx-action@v3
116
-
117
- - name: Log in to Docker Hub
118
- uses: docker/login-action@v3
119
- with:
120
- username: ${{ secrets.DOCKERHUB_ORG }}
121
- password: ${{ secrets.DOCKERHUB_OAT_TOKEN }}
122
-
123
- - name: Re-resolve Docker tags for this build
124
- id: resolve
125
- env:
126
- EVENT_NAME: ${{ github.event_name }}
127
- SOURCE_REF_NAME: ${{ github.ref_name }}
128
- SOURCE_REF_TYPE: ${{ github.ref_type }}
129
- BEFORE_SHA: ${{ github.event_name == 'push' && github.event.before || '' }}
130
- AFTER_SHA: ${{ github.event_name == 'push' && github.sha || '' }}
131
- MANUAL_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
132
- DOCKER_IMAGE_REPO: ${{ format('{0}/{1}', secrets.DOCKERHUB_ORG, env.DOCKER_IMAGE_NAME) }}
133
- TARGET_BRANCH: ${{ matrix.branch }}
134
- TARGET_TAG: ${{ matrix.source_tag }}
135
- TARGET_MODE: ${{ matrix.mode }}
136
- TARGET_PUBLISH_VERSION: ${{ matrix.publish_version }}
137
- TARGET_PUBLISH_BRANCH_TAG: ${{ matrix.publish_branch_tag }}
138
- run: python3 .github/scripts/docker_release_plan.py resolve-build
139
-
140
- - name: Skip when target is no longer eligible
141
- if: steps.resolve.outputs.should_build != 'true'
142
- run: echo "${{ steps.resolve.outputs.skip_reason }}"
143
-
144
- - name: Set cache date
145
- if: steps.resolve.outputs.should_build == 'true'
146
- id: cache_date
147
- run: echo "value=$(date -u +%Y-%m-%d:%H:%M:%S)" >> "$GITHUB_OUTPUT"
148
-
149
- - name: Build and push Docker image
150
- if: steps.resolve.outputs.should_build == 'true'
151
- uses: docker/build-push-action@v6
152
- with:
153
- context: ${{ env.DOCKERFILE_DIR }}
154
- file: ${{ env.DOCKERFILE_PATH }}
155
- platforms: ${{ env.DOCKER_PLATFORMS }}
156
- push: true
157
- tags: ${{ steps.resolve.outputs.tags }}
158
- build-args: |
159
- BRANCH=${{ matrix.branch }}
160
- CACHE_DATE=${{ steps.cache_date.outputs.value }}
161
-
162
- - name: Resolve GitHub release target
163
- if: steps.resolve.outputs.should_build == 'true'
164
- id: release_plan
165
- env:
166
- EVENT_NAME: ${{ github.event_name }}
167
- SOURCE_REF_NAME: ${{ github.ref_name }}
168
- SOURCE_REF_TYPE: ${{ github.ref_type }}
169
- BEFORE_SHA: ${{ github.event_name == 'push' && github.event.before || '' }}
170
- AFTER_SHA: ${{ github.event_name == 'push' && github.sha || '' }}
171
- MANUAL_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
172
- DOCKER_IMAGE_REPO: ${{ format('{0}/{1}', secrets.DOCKERHUB_ORG, env.DOCKER_IMAGE_NAME) }}
173
- TARGET_BRANCH: ${{ matrix.branch }}
174
- TARGET_TAG: ${{ matrix.source_tag }}
175
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
176
- OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
177
- OPENROUTER_MODEL_NAME: ${{ vars.OPENROUTER_MODEL_NAME }}
178
- run: python3 .github/scripts/docker_release_plan.py resolve-release
179
-
180
- - name: Skip GitHub release
181
- if: steps.resolve.outputs.should_build == 'true' && steps.release_plan.outputs.should_release != 'true'
182
- run: echo "${{ steps.release_plan.outputs.skip_reason }}"
183
-
184
- - name: Create or update GitHub release
185
- if: steps.resolve.outputs.should_build == 'true' && steps.release_plan.outputs.should_release == 'true'
186
- uses: actions/github-script@v7
187
- env:
188
- RELEASE_TAG: ${{ steps.release_plan.outputs.release_tag }}
189
- RELEASE_NAME: ${{ steps.release_plan.outputs.release_name }}
190
- RELEASE_BODY: ${{ steps.release_plan.outputs.release_body }}
191
- with:
192
- script: |
193
- const owner = context.repo.owner;
194
- const repo = context.repo.repo;
195
- const tag = process.env.RELEASE_TAG;
196
- const name = process.env.RELEASE_NAME;
197
- const body = process.env.RELEASE_BODY;
198
-
199
- try {
200
- const existing = await github.rest.repos.getReleaseByTag({
201
- owner,
202
- repo,
203
- tag,
204
- });
205
-
206
- await github.rest.repos.updateRelease({
207
- owner,
208
- repo,
209
- release_id: existing.data.id,
210
- tag_name: tag,
211
- name,
212
- body,
213
- draft: false,
214
- prerelease: false,
215
- make_latest: "true",
216
- });
217
-
218
- core.info(`Updated release ${tag}`);
219
- } catch (error) {
220
- if (error.status !== 404) {
221
- throw error;
222
- }
223
-
224
- await github.rest.repos.createRelease({
225
- owner,
226
- repo,
227
- tag_name: tag,
228
- name,
229
- body,
230
- draft: false,
231
- prerelease: false,
232
- make_latest: "true",
233
- });
234
-
235
- core.info(`Created release ${tag}`);
236
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:49dd4916573cc8dad39525a67b637e9879a0c222c97b834f5e72c7ed02e50a20
3
+ size 8548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -1,52 +1,3 @@
1
- # Ignore common unwanted files globally
2
- **/.DS_Store
3
- **/.env
4
- **/__pycache__/
5
- *.py[cod]
6
- **/.conda/
7
- **/node_modules/
8
-
9
- #Ignore IDE files
10
- .cursor/
11
- .windsurf/
12
-
13
- # ignore test files in root dir
14
- /*.test.py
15
-
16
- # Ignore all contents of the virtual environment directory
17
- .venv/
18
-
19
- # obsolete folders
20
- /memory/
21
- /knowledge/custom/
22
- /instruments/
23
-
24
- # Handle logs directory
25
- logs/**
26
- !logs/**/
27
-
28
- # Handle tmp and usr directory
29
- tmp/**
30
- !tmp/**/
31
-
32
- # hack to keep .gitkeep but ignore nested repos
33
- # Ignore everything under usr
34
- usr/**
35
- # Ignore nested repos
36
- /usr/**/.git
37
- # Allow git to traverse directories
38
- !usr/**/
39
- # Re-ignore everything again
40
- usr/**/*
41
- # But allow .gitkeep files
42
- !usr/**/.gitkeep
43
-
44
-
45
- # Global rule to include .gitkeep files anywhere
46
- !**/.gitkeep
47
-
48
- # for browser-use
49
- agent_history.gif
50
-
51
- .agent/**
52
- .claude/**
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5760fba62009e975160fd6830995de30e75b3ca05ad6b7c2f69687e7691d050a
3
+ size 789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.vscode/extensions.json CHANGED
@@ -1,7 +1,3 @@
1
- {
2
- "recommendations": [
3
- "usernamehw.errorlens",
4
- "ms-python.debugpy",
5
- "ms-python.python"
6
- ]
7
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c372492c38c6ff5040d4de969bde64752703fac076d237fd6f5cb418d28195db
3
+ size 122
 
 
 
 
.vscode/launch.json CHANGED
@@ -1,24 +1,3 @@
1
- {
2
- "version": "0.2.0",
3
- "configurations": [
4
-
5
- {
6
- "name": "Debug run_ui.py",
7
- "type": "debugpy",
8
- "request": "launch",
9
- "program": "./run_ui.py",
10
- "console": "integratedTerminal",
11
- "justMyCode": false,
12
- "args": ["--development=true", "-Xfrozen_modules=off"]
13
- },
14
- {
15
- "name": "Debug current file",
16
- "type": "debugpy",
17
- "request": "launch",
18
- "program": "${file}",
19
- "console": "integratedTerminal",
20
- "justMyCode": false,
21
- "args": ["--development=true", "-Xfrozen_modules=off"]
22
- }
23
- ]
24
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:863ede1d1e561787d4333cd4d4e2d71581ef6c49ebe5cf8dcec5f2ba3e693c7a
3
+ size 565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.vscode/settings.json CHANGED
@@ -1,17 +1,3 @@
1
- {
2
- "python.analysis.typeCheckingMode": "standard",
3
- "windsurfPyright.analysis.diagnosticMode": "workspace",
4
- "windsurfPyright.analysis.typeCheckingMode": "standard",
5
- // Enable JavaScript linting
6
- "eslint.enable": true,
7
- "eslint.validate": ["javascript", "javascriptreact"],
8
- // Set import root for JS/TS
9
- "javascript.preferences.importModuleSpecifier": "relative",
10
- "js/ts.implicitProjectConfig.checkJs": true,
11
- "jsconfig.paths": {
12
- "*": ["webui/*"]
13
- },
14
- // Optional: point VSCode to jsconfig.json if you add one
15
- "jsconfig.json": "${workspaceFolder}/jsconfig.json",
16
- "postman.settings.dotenv-detection-notification-visibility": false
17
- }
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9d702ba4601b5cf95f3738524887ceebd9163fbc349c53d367f7616f2fc0387a
3
+ size 686
 
 
 
 
 
 
 
 
 
 
 
 
 
 
AGENTS.md CHANGED
@@ -1,253 +1,3 @@
1
- # Agent Zero - AGENTS.md
2
-
3
- [Generated using reconnaissance on 2026-02-22]
4
-
5
- ## Quick Reference
6
- Tech Stack: Python 3.12+ | Flask | Alpine.js | LiteLLM | WebSocket (Socket.io)
7
- Dev Server: python run_ui.py (runs on http://localhost:50001 by default)
8
- Run Tests: pytest (standard) or pytest tests/test_name.py (file-scoped)
9
- Documentation: README.md | docs/
10
- Frontend Deep Dives: [Component System](docs/agents/AGENTS.components.md) | [Modal System](docs/agents/AGENTS.modals.md) | [Plugin Architecture](docs/agents/AGENTS.plugins.md)
11
-
12
- ---
13
-
14
- ## Table of Contents
15
- 1. [Project Overview](#project-overview)
16
- 2. [Core Commands](#core-commands)
17
- 3. [Docker Environment](#docker-environment)
18
- 4. [Project Structure](#project-structure)
19
- 5. [Development Patterns & Conventions](#development-patterns--conventions)
20
- 6. [Safety and Permissions](#safety-and-permissions)
21
- 7. [Code Examples](#code-examples)
22
- 8. [Git Workflow](#git-workflow)
23
- 9. [Release Notes](#release-notes)
24
- 10. [Troubleshooting](#troubleshooting)
25
-
26
- ---
27
-
28
- ## Project Overview
29
-
30
- Agent Zero is a dynamic, organic agentic framework designed to grow and learn. It uses the operating system as a tool, featuring a multi-agent cooperation model where every agent can create subordinates to break down tasks.
31
-
32
- Type: Full-Stack Agentic Framework (Python Backend + Alpine.js Frontend)
33
- Status: Active Development
34
- Primary Language(s): Python, JavaScript (ES Modules)
35
-
36
- ---
37
-
38
- ## Core Commands
39
-
40
- ### Setup
41
- Do not combine these commands; run them individually:
42
- ```bash
43
- pip install -r requirements.txt
44
- pip install -r requirements2.txt
45
- ```
46
- - Start WebUI: python run_ui.py
47
-
48
- ---
49
-
50
- ## Docker Environment
51
-
52
- When running in Docker, Agent Zero uses two distinct Python runtimes to isolate the framework from the code being executed:
53
-
54
- ### 1. Framework Runtime (/opt/venv-a0)
55
- - Version: Python 3.12.4
56
- - Purpose: Runs the Agent Zero backend, API, and core logic.
57
- - Packages: Contains all dependencies from requirements.txt.
58
-
59
- ### 2. Execution Runtime (/opt/venv)
60
- - Version: Python 3.13
61
- - Purpose: Default environment for the interactive terminal and the agent's code execution tool.
62
- - Behavior: This is the environment active when you docker exec into the container. Packages installed by the agent via pip install during a task are stored here.
63
-
64
- ---
65
-
66
- ## Project Structure
67
-
68
- ```
69
- /
70
- ├── agent.py # Core Agent and AgentContext definitions
71
- ├── initialize.py # Framework initialization logic
72
- ├── models.py # LLM provider configurations
73
- ├── run_ui.py # WebUI server entry point
74
- ├── api/ # API Handlers (ApiHandler subclasses) + WsHandler subclasses (ws_*.py)
75
- ├── extensions/ # Backend lifecycle extensions
76
- ├── helpers/ # Shared Python utilities (plugins, files, etc.)
77
- ├── tools/ # Agent tools (Tool subclasses)
78
- ├── webui/
79
- │ ├── components/ # Alpine.js components
80
- │ ├── js/ # Core frontend logic (modals, stores, etc.)
81
- │ └── index.html # Main UI shell
82
- ├── usr/ # User data directory (isolated from core)
83
- │ ├── plugins/ # Custom user plugins
84
- │ ├── settings.json # User-specific configuration
85
- │ └── workdir/ # Default agent workspace
86
- ├── plugins/ # Core system plugins
87
- ├── agents/ # Agent profiles (prompts and config)
88
- ├── prompts/ # System and message prompt templates
89
- ├── knowledge/
90
- │ └── main/about/ # Agent self-knowledge (indexed into vector DB for runtime recall)
91
- │ ├── identity.md # Philosophy, principles, project context
92
- │ ├── architecture.md # Agent loop, memory pipeline, multi-agent, extensions
93
- │ ├── capabilities.md # Detailed capabilities and limitations
94
- │ ├── configuration.md # LLM roles, providers, profiles, plugins, settings
95
- │ └── setup-and-deployment.md # Docker deployment, updates, troubleshooting
96
- └── tests/ # Pytest suite
97
- ```
98
-
99
- Key Files:
100
- - agent.py: Defines AgentContext and the main Agent class.
101
- - helpers/plugins.py: Plugin discovery and configuration logic.
102
- - webui/js/AlpineStore.js: Store factory for reactive frontend state.
103
- - helpers/api.py: Base class for all API endpoints.
104
- - scripts/openrouter_release_notes_system_prompt.md: Editable system prompt used to generate GitHub release notes during Docker publishing.
105
- - knowledge/main/about/: Agent self-knowledge files, indexed into the vector DB for runtime recall. Not user-facing docs - written for the agent's internal reference.
106
- - docs/agents/AGENTS.components.md: Deep dive into the frontend component architecture.
107
- - docs/agents/AGENTS.modals.md: Guide to the stacked modal system.
108
- - docs/agents/AGENTS.plugins.md: Comprehensive guide to the full-stack plugin system.
109
-
110
- ---
111
-
112
- ## Development Patterns & Conventions
113
-
114
- ### Backend (Python)
115
- - Context Access: Use from agent import AgentContext, AgentContextType (not helpers.context).
116
- - Communication: Use mq from helpers.messages to log proactive UI messages:
117
- mq.log_user_message(context.id, "Message", source="Plugin")
118
- - API Handlers: Derive from ApiHandler in helpers/api.py.
119
- - Extensions: Use the extension framework in helpers/extension.py for lifecycle hooks.
120
- - Error Handling: Use RepairableException for errors the LLM might be able to fix.
121
-
122
- ### Frontend (Alpine.js)
123
- - Store Gating: Always wrap store-dependent content in a template:
124
- ```html
125
- <div x-data>
126
- <template x-if="$store.myStore">
127
- <div x-init="$store.myStore.onOpen()">...</div>
128
- </template>
129
- </div>
130
- ```
131
- - Store Registration: Use createStore from /js/AlpineStore.js.
132
- - Modals: Use openModal(path) and closeModal() from /js/modals.js.
133
-
134
- ### Plugin Architecture
135
- - Location: Always develop new plugins in usr/plugins/.
136
- - Manifest: Every plugin requires a plugin.yaml with name, description, version, and optionally settings_sections, per_project_config, per_agent_config, and always_enabled.
137
- - Discovery: Conventions based on folder names (api/, tools/, webui/, extensions/).
138
- - Plugin-local Python imports: Prefer `usr.plugins.<plugin_name>...` for code that lives under `usr/plugins/`. Avoid `sys.path` hacks and avoid symlink-dependent `plugins.<plugin_name>...` imports for community plugins.
139
- - Runtime hooks: Plugins may also expose hooks in hooks.py, callable by the framework through helpers.plugins.call_plugin_hook(...).
140
- - Hook runtime: hooks.py executes inside the Agent Zero framework Python environment, so sys.executable -m pip installs dependencies into that same framework runtime.
141
- - Environment targeting: If a plugin needs packages or binaries for the separate agent execution runtime or system environment, it must explicitly switch environments in a subprocess by targeting the correct interpreter, virtualenv, or package manager.
142
- - Settings: Use get_plugin_config(plugin_name, agent=agent) to retrieve settings. Plugins can expose a UI for settings via webui/config.html. Plugin settings modals instantiate a local context from $store.pluginSettingsPrototype; bind plugin fields to config.* and use context.* for modal-level state and actions.
143
- - Activation: Global and scoped activation rules are stored as .toggle-1 (ON) and .toggle-0 (OFF). Scoped rules are handled via the plugin "Switch" modal.
144
- - Cleanup rule: Plugins should not permanently modify the system in ways that outlive the plugin. Deleting a plugin should not leave behind symlinks, unmanaged services, or stray files outside plugin-owned paths unless the user explicitly requested that behavior.
145
-
146
- ### Releases
147
- - Docker publishing automation lives in `.github/workflows/docker-publish.yml`.
148
- - Releasable tags follow `v{X}.{Y}` and only tags `>= v1.0` are considered by the workflow.
149
- - The latest eligible tag on `main` also creates or updates a GitHub release after the Docker image push succeeds.
150
- - GitHub release notes are generated on the fly in `.github/scripts/docker_release_plan.py` by comparing the new tag against the previous published GitHub release tag, collecting commit subjects and descriptions in that range, and sending them to OpenRouter.
151
- - The OpenRouter call uses `OPENROUTER_API_KEY` and `OPENROUTER_MODEL_NAME` from the workflow environment, with the system prompt stored in `scripts/openrouter_release_notes_system_prompt.md`.
152
- - Prioritize user-visible features, important fixes, infra or packaging changes, and breaking notes. Skip low-signal churn.
153
- - If the generated summary has no meaningful content, the release body falls back to `No release notes.`
154
-
155
- ### Lifecycle Synchronization
156
- | Action | Backend Extension | Frontend Lifecycle |
157
- |---|---|---|
158
- | Initialization | agent_init | init() in Store |
159
- | Mounting | N/A | x-create directive |
160
- | Processing | monologue_start/end | UI loading state |
161
- | Cleanup | context_deleted | x-destroy directive |
162
-
163
- ---
164
-
165
- ## Safety and Permissions
166
-
167
- ### Allowed Without Asking
168
- - Read any file in the repository.
169
- - Update code files in usr/.
170
-
171
- ### Ask Before Executing
172
- - pip install (new dependencies).
173
- - Deleting core files outside of usr/ or tmp/.
174
- - Modifying agent.py or initialize.py.
175
- - Making git commits or pushes.
176
-
177
- ### Never Do
178
- - Commit, hardcode or leak secrets or .env files.
179
- - Bypass CSRF or authentication checks.
180
- - Hardcode API keys.
181
-
182
- ---
183
-
184
- ## Code Examples
185
-
186
- ### API Handler (Good)
187
- ```python
188
- from helpers.api import ApiHandler, Request, Response
189
-
190
- class MyHandler(ApiHandler):
191
- async def process(self, input: dict, request: Request) -> dict | Response:
192
- # Business logic here
193
- return {"ok": True, "data": "result"}
194
- ```
195
-
196
- ### Alpine Store (Good)
197
- ```javascript
198
- import { createStore } from "/js/AlpineStore.js";
199
-
200
- export const store = createStore("myStore", {
201
- items: [],
202
- init() { /* global setup */ },
203
- onOpen() { /* mount setup */ },
204
- cleanup() { /* unmount cleanup */ }
205
- });
206
- ```
207
-
208
- ### Tool Definition (Good)
209
- ```python
210
- from helpers.tool import Tool, Response
211
-
212
- class MyTool(Tool):
213
- async def execute(self, **kwargs):
214
- # Tool logic
215
- return Response(message="Success", break_loop=False)
216
- ```
217
-
218
- ---
219
-
220
- ## Git Workflow
221
-
222
- - Docker publish automation lives in `.github/workflows/docker-publish.yml`.
223
- - Release tags handled by automation must match `vX.Y` and be `>= v1.0`.
224
- - Allowed release branches are configured at the top of the workflow. `main` publishes `<tag>` and `latest`; other allowed branches publish only the branch tag.
225
- - Manual dispatch accepts an optional tag. Without a tag it backfills missing Docker Hub tags. With a tag it rebuilds that exact target and only refreshes `latest` and the GitHub release when that tag is still the newest eligible tag on `main`.
226
-
227
- ---
228
-
229
- ## Release Notes
230
-
231
- - The latest eligible `main` tag generates its GitHub release notes during Docker publish instead of reading committed Markdown files.
232
- - The release-note prompt is editable in `scripts/openrouter_release_notes_system_prompt.md`.
233
- - The commit range starts at the previous published GitHub release tag, not merely the previous semantic tag in the repository.
234
-
235
- ## Troubleshooting
236
-
237
- ### Dependency Conflicts
238
- If pip install fails, try running in a clean virtual environment:
239
- ```bash
240
- python -m venv .venv
241
- source .venv/bin/activate
242
- pip install -r requirements.txt
243
- pip install -r requirements2.txt
244
- ```
245
-
246
- ### WebSocket Connection Failures
247
- - Check if X-CSRF-Token is being sent.
248
- - Ensure the runtime ID in the session matches the current server instance.
249
-
250
- ---
251
-
252
- *Last updated: 2026-03-25*
253
- *Maintained by: Agent Zero Core Team*
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7854c02496e343196eab895f1fe44bfa34af38041878054b078c22d7200dcc6f
3
+ size 11578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DockerfileLocal CHANGED
@@ -1,36 +1,3 @@
1
- # Use the pre-built base image for A0
2
- # FROM agent-zero-base:local
3
- FROM agent0ai/agent-zero-base:latest
4
-
5
- # Set BRANCH to "local" if not provided
6
- ARG BRANCH=local
7
- ENV BRANCH=$BRANCH
8
-
9
- # Copy filesystem files to root
10
- COPY ./docker/run/fs/ /
11
- # Copy current development files to git, they will only be used in "local" branch
12
- COPY ./ /git/agent-zero
13
-
14
- # pre installation steps
15
- RUN bash /ins/pre_install.sh $BRANCH
16
-
17
- # install A0
18
- RUN bash /ins/install_A0.sh $BRANCH
19
-
20
- # install additional software
21
- RUN bash /ins/install_additional.sh $BRANCH
22
-
23
- # cleanup repo and install A0 without caching, this speeds up builds
24
- ARG CACHE_DATE=none
25
- RUN echo "cache buster $CACHE_DATE" && bash /ins/install_A02.sh $BRANCH
26
-
27
- # post installation steps
28
- RUN bash /ins/post_install.sh $BRANCH
29
-
30
- # Expose ports
31
- EXPOSE 22 80 9000-9009
32
-
33
- RUN chmod +x /exe/initialize.sh /exe/run_A0.sh /exe/run_searxng.sh /exe/run_tunnel_api.sh
34
-
35
- # initialize runtime and switch to supervisord
36
- CMD ["/exe/initialize.sh", "$BRANCH"]
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9083efc3881bf1b7edb950eca369e75c380628ee372cb5290cd0939a7459b236
3
+ size 975
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LICENSE CHANGED
@@ -1,23 +1,3 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Agent Zero, s.r.o
4
- Contact: pr@agent-zero.ai
5
- Repository: https://github.com/agent0ai/agent-zero
6
-
7
- Permission is hereby granted, free of charge, to any person obtaining a copy
8
- of this software and associated documentation files (the "Software"), to deal
9
- in the Software without restriction, including without limitation the rights
10
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- copies of the Software, and to permit persons to whom the Software is
12
- furnished to do so, subject to the following conditions:
13
-
14
- The above copyright notice and this permission notice shall be included in all
15
- copies or substantial portions of the Software.
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- SOFTWARE.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:23844ed5fb9976b15e6c0be1b617918cb1c32e13e8d70b74100dae08d40fbf23
3
+ size 1150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent.py CHANGED
@@ -1,1023 +1,3 @@
1
- import asyncio, random, string, threading
2
-
3
- from collections import OrderedDict
4
- from dataclasses import dataclass, field
5
- from datetime import datetime, timezone
6
- from typing import Any, Awaitable, Coroutine, Dict, Literal
7
- from enum import Enum
8
- import models
9
-
10
- from helpers import (
11
- extract_tools,
12
- files,
13
- errors,
14
- history,
15
- tokens,
16
- context as context_helper,
17
- dirty_json,
18
- subagents,
19
- )
20
- from helpers import extension
21
- from helpers.print_style import PrintStyle
22
-
23
- from langchain_core.prompts import (
24
- ChatPromptTemplate,
25
- )
26
- from langchain_core.messages import SystemMessage, BaseMessage
27
-
28
- import helpers.log as Log
29
- from helpers.dirty_json import DirtyJson
30
- from helpers.defer import DeferredTask
31
- from typing import Callable
32
- from helpers.localization import Localization
33
- from helpers import extension
34
- from helpers.errors import RepairableException, InterventionException, HandledException
35
-
36
- class AgentContextType(Enum):
37
- USER = "user"
38
- TASK = "task"
39
- BACKGROUND = "background"
40
-
41
-
42
- class AgentContext:
43
-
44
- _contexts: dict[str, "AgentContext"] = {}
45
- _contexts_lock = threading.RLock()
46
- _counter: int = 0
47
- _notification_manager = None
48
-
49
- @extension.extensible
50
- def __init__(
51
- self,
52
- config: "AgentConfig",
53
- id: str | None = None,
54
- name: str | None = None,
55
- agent0: "Agent|None" = None,
56
- log: Log.Log | None = None,
57
- paused: bool = False,
58
- streaming_agent: "Agent|None" = None,
59
- created_at: datetime | None = None,
60
- type: AgentContextType = AgentContextType.USER,
61
- last_message: datetime | None = None,
62
- data: dict | None = None,
63
- output_data: dict | None = None,
64
- set_current: bool = False,
65
- ):
66
- # initialize context
67
- self.id = id or AgentContext.generate_id()
68
- existing = None
69
- with AgentContext._contexts_lock:
70
- existing = AgentContext._contexts.get(self.id, None)
71
- if existing:
72
- AgentContext._contexts.pop(self.id, None)
73
- AgentContext._contexts[self.id] = self
74
- if existing and existing.task:
75
- existing.task.kill()
76
- if set_current:
77
- AgentContext.set_current(self.id)
78
-
79
- # initialize state
80
- self.name = name
81
- self.config = config
82
- self.data = data or {}
83
- self.output_data = output_data or {}
84
- self.log = log or Log.Log()
85
- self.log.context = self
86
- self.paused = paused
87
- self.streaming_agent = streaming_agent
88
- self.task: DeferredTask | None = None
89
- self.created_at = created_at or datetime.now(timezone.utc)
90
- self.type = type
91
- AgentContext._counter += 1
92
- self.no = AgentContext._counter
93
- self.last_message = last_message or datetime.now(timezone.utc)
94
-
95
- # initialize agent at last (context is complete now)
96
- self.agent0 = agent0 or Agent(0, self.config, self)
97
-
98
- @staticmethod
99
- def get(id: str):
100
- with AgentContext._contexts_lock:
101
- return AgentContext._contexts.get(id, None)
102
-
103
- @staticmethod
104
- def use(id: str):
105
- context = AgentContext.get(id)
106
- if context:
107
- AgentContext.set_current(id)
108
- else:
109
- AgentContext.set_current("")
110
- return context
111
-
112
- @staticmethod
113
- def current():
114
- ctxid = context_helper.get_context_data("agent_context_id", "")
115
- if not ctxid:
116
- return None
117
- return AgentContext.get(ctxid)
118
-
119
- @staticmethod
120
- def set_current(ctxid: str):
121
- context_helper.set_context_data("agent_context_id", ctxid)
122
-
123
- @staticmethod
124
- def first():
125
- with AgentContext._contexts_lock:
126
- if not AgentContext._contexts:
127
- return None
128
- return list(AgentContext._contexts.values())[0]
129
-
130
- @staticmethod
131
- def all():
132
- with AgentContext._contexts_lock:
133
- return list(AgentContext._contexts.values())
134
-
135
- @staticmethod
136
- def generate_id():
137
- def generate_short_id():
138
- return "".join(random.choices(string.ascii_letters + string.digits, k=8))
139
-
140
- while True:
141
- short_id = generate_short_id()
142
- with AgentContext._contexts_lock:
143
- if short_id not in AgentContext._contexts:
144
- return short_id
145
-
146
- @classmethod
147
- def get_notification_manager(cls):
148
- if cls._notification_manager is None:
149
- from helpers.notification import NotificationManager # type: ignore
150
-
151
- cls._notification_manager = NotificationManager()
152
- return cls._notification_manager
153
-
154
- @staticmethod
155
- @extension.extensible
156
- def remove(id: str):
157
- with AgentContext._contexts_lock:
158
- context = AgentContext._contexts.pop(id, None)
159
- if context and context.task:
160
- context.task.kill()
161
- return context
162
-
163
- def get_data(self, key: str, recursive: bool = True):
164
- # recursive is not used now, prepared for context hierarchy
165
- return self.data.get(key, None)
166
-
167
- def set_data(self, key: str, value: Any, recursive: bool = True):
168
- # recursive is not used now, prepared for context hierarchy
169
- self.data[key] = value
170
-
171
- def get_output_data(self, key: str, recursive: bool = True):
172
- # recursive is not used now, prepared for context hierarchy
173
- return self.output_data.get(key, None)
174
-
175
- def set_output_data(self, key: str, value: Any, recursive: bool = True):
176
- # recursive is not used now, prepared for context hierarchy
177
- self.output_data[key] = value
178
-
179
- # @extension.extensible
180
- def output(self):
181
- return {
182
- "id": self.id,
183
- "name": self.name,
184
- "created_at": (
185
- Localization.get().serialize_datetime(self.created_at)
186
- if self.created_at
187
- else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
188
- ),
189
- "no": self.no,
190
- "log_guid": self.log.guid,
191
- "log_version": len(self.log.updates),
192
- "log_length": len(self.log.logs),
193
- "paused": self.paused,
194
- "last_message": (
195
- Localization.get().serialize_datetime(self.last_message)
196
- if self.last_message
197
- else Localization.get().serialize_datetime(datetime.fromtimestamp(0))
198
- ),
199
- "type": self.type.value,
200
- "running": self.is_running(),
201
- **self.output_data,
202
- }
203
-
204
- @staticmethod
205
- def log_to_all(
206
- type: Log.Type,
207
- heading: str | None = None,
208
- content: str | None = None,
209
- kvps: dict | None = None,
210
- update_progress: Log.ProgressUpdate | None = None,
211
- id: str | None = None, # Add id parameter
212
- **kwargs,
213
- ) -> list[Log.LogItem]:
214
- items: list[Log.LogItem] = []
215
- for context in AgentContext.all():
216
- items.append(
217
- context.log.log(
218
- type, heading, content, kvps, update_progress, id, **kwargs
219
- )
220
- )
221
- return items
222
-
223
- @extension.extensible
224
- def kill_process(self):
225
- if self.task:
226
- self.task.kill()
227
-
228
- @extension.extensible
229
- def reset(self):
230
- self.kill_process()
231
- self.log.reset()
232
- self.agent0 = Agent(0, self.config, self)
233
- self.streaming_agent = None
234
- self.paused = False
235
-
236
- @extension.extensible
237
- def nudge(self):
238
- self.kill_process()
239
- self.paused = False
240
- self.task = self.communicate(UserMessage(self.agent0.read_prompt("fw.msg_nudge.md")))
241
- return self.task
242
-
243
- @extension.extensible
244
- def get_agent(self):
245
- return self.streaming_agent or self.agent0
246
-
247
- def is_running(self) -> bool:
248
- return (self.task and self.task.is_alive()) or False
249
-
250
- @extension.extensible
251
- def communicate(self, msg: "UserMessage", broadcast_level: int = 1):
252
- self.paused = False # unpause if paused
253
-
254
- current_agent = self.get_agent()
255
-
256
- if self.task and self.task.is_alive():
257
- # set intervention messages to agent(s):
258
- intervention_agent = current_agent
259
- while intervention_agent and broadcast_level != 0:
260
- intervention_agent.intervention = msg
261
- broadcast_level -= 1
262
- intervention_agent = intervention_agent.data.get(
263
- Agent.DATA_NAME_SUPERIOR, None
264
- )
265
- else:
266
- self.task = self.run_task(self._process_chain, current_agent, msg)
267
-
268
- return self.task
269
-
270
- @extension.extensible
271
- def run_task(
272
- self, func: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any
273
- ):
274
- if not self.task:
275
- self.task = DeferredTask(
276
- thread_name=self.__class__.__name__,
277
- )
278
- self.task.start_task(func, *args, **kwargs)
279
- return self.task
280
-
281
- # this wrapper ensures that superior agents are called back if the chat was loaded from file and original callstack is gone
282
- @extension.extensible
283
- async def _process_chain(self, agent: "Agent", msg: "UserMessage|str", user=True):
284
- try:
285
- msg_template = (
286
- agent.hist_add_user_message(msg) # type: ignore
287
- if user
288
- else agent.hist_add_tool_result(
289
- tool_name="call_subordinate", tool_result=msg # type: ignore
290
- )
291
- )
292
- response = await agent.monologue() # type: ignore
293
- superior = agent.data.get(Agent.DATA_NAME_SUPERIOR, None)
294
- if superior:
295
- response = await self._process_chain(superior, response, False) # type: ignore
296
-
297
- # call end of process extensions
298
- await extension.call_extensions_async("process_chain_end", agent=self.get_agent(), data={})
299
-
300
- return response
301
- except Exception as e:
302
- await self.handle_exception("process_chain", e)
303
-
304
- @extension.extensible
305
- async def handle_exception(self, location: str, exception: Exception):
306
- if exception:
307
- raise exception # exception handling is done by extensions
308
-
309
-
310
- @dataclass
311
- class AgentConfig:
312
- mcp_servers: str
313
- profile: str = ""
314
- knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
315
- additional: Dict[str, Any] = field(default_factory=dict)
316
-
317
-
318
- @dataclass
319
- class UserMessage:
320
- message: str
321
- attachments: list[str] = field(default_factory=list[str])
322
- system_message: list[str] = field(default_factory=list[str])
323
- id: str = ""
324
-
325
-
326
- class LoopData:
327
- def __init__(self, **kwargs):
328
- self.iteration = -1
329
- self.system = []
330
- self.user_message: history.Message | None = None
331
- self.history_output: list[history.OutputMessage] = []
332
- self.extras_temporary: OrderedDict[str, history.MessageContent] = OrderedDict()
333
- self.extras_persistent: OrderedDict[str, history.MessageContent] = OrderedDict()
334
- self.last_response = ""
335
- self.params_temporary: dict = {}
336
- self.params_persistent: dict = {}
337
- self.current_tool = None
338
-
339
- # override values with kwargs
340
- for key, value in kwargs.items():
341
- setattr(self, key, value)
342
-
343
-
344
- class Agent:
345
-
346
- DATA_NAME_SUPERIOR = "_superior"
347
- DATA_NAME_SUBORDINATE = "_subordinate"
348
- DATA_NAME_CTX_WINDOW = "ctx_window"
349
-
350
- @extension.extensible
351
- def __init__(
352
- self, number: int, config: AgentConfig, context: AgentContext | None = None
353
- ):
354
-
355
- # agent config
356
- self.config = config
357
-
358
- # agent context
359
- self.context = context or AgentContext(config=config, agent0=self)
360
-
361
- # non-config vars
362
- self.number = number
363
- self.agent_name = f"A{self.number}"
364
-
365
- self.history = history.History(self) # type: ignore[abstract]
366
- self.last_user_message: history.Message | None = None
367
- self.intervention: UserMessage | None = None
368
- self.data: dict[str, Any] = {} # free data object all the tools can use
369
-
370
- extension.call_extensions_sync("agent_init", self)
371
-
372
- @extension.extensible
373
- async def monologue(self):
374
- while True:
375
- try:
376
- # loop data dictionary to pass to extensions
377
- self.loop_data = LoopData(user_message=self.last_user_message)
378
- # call monologue_start extensions
379
- await extension.call_extensions_async(
380
- "monologue_start", self, loop_data=self.loop_data
381
- )
382
-
383
- printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
384
-
385
- # let the agent run message loop until he stops it with a response tool
386
- while True:
387
-
388
- self.context.streaming_agent = self # mark self as current streamer
389
- self.loop_data.iteration += 1
390
- self.loop_data.params_temporary = {} # clear temporary params
391
-
392
- # call message_loop_start extensions
393
- await extension.call_extensions_async(
394
- "message_loop_start", self, loop_data=self.loop_data
395
- )
396
- await self.handle_intervention()
397
-
398
- try:
399
- # prepare LLM chain (model, system, history)
400
- prompt = await self.prepare_prompt(loop_data=self.loop_data)
401
-
402
- # call before_main_llm_call extensions
403
- await extension.call_extensions_async(
404
- "before_main_llm_call", self, loop_data=self.loop_data
405
- )
406
- await self.handle_intervention()
407
-
408
-
409
- async def reasoning_callback(chunk: str, full: str):
410
- await self.handle_intervention()
411
- if chunk == full:
412
- printer.print("Reasoning: ") # start of reasoning
413
- # Pass chunk and full data to extensions for processing
414
- stream_data = {"chunk": chunk, "full": full}
415
- await extension.call_extensions_async(
416
- "reasoning_stream_chunk",
417
- self,
418
- loop_data=self.loop_data,
419
- stream_data=stream_data,
420
- )
421
- # Stream masked chunk after extensions processed it
422
- if stream_data.get("chunk"):
423
- printer.stream(stream_data["chunk"])
424
- # Use the potentially modified full text for downstream processing
425
- await self.handle_reasoning_stream(stream_data["full"])
426
-
427
- async def stream_callback(chunk: str, full: str):
428
- await self.handle_intervention()
429
- # output the agent response stream
430
- if chunk == full:
431
- printer.print("Response: ") # start of response
432
- # Pass chunk and full data to extensions for processing
433
- stream_data = {"chunk": chunk, "full": full}
434
- await extension.call_extensions_async(
435
- "response_stream_chunk",
436
- self,
437
- loop_data=self.loop_data,
438
- stream_data=stream_data,
439
- )
440
- # Stream masked chunk after extensions processed it
441
- if stream_data.get("chunk"):
442
- printer.stream(stream_data["chunk"])
443
- # Use the potentially modified full text for downstream processing
444
- await self.handle_response_stream(stream_data["full"])
445
-
446
- # call main LLM
447
- agent_response, _reasoning = await self.call_chat_model(
448
- messages=prompt,
449
- response_callback=stream_callback,
450
- reasoning_callback=reasoning_callback,
451
- )
452
- await self.handle_intervention(agent_response)
453
-
454
- # Notify extensions to finalize their stream filters
455
- await extension.call_extensions_async(
456
- "reasoning_stream_end", self, loop_data=self.loop_data
457
- )
458
- await self.handle_intervention(agent_response)
459
-
460
- await extension.call_extensions_async(
461
- "response_stream_end", self, loop_data=self.loop_data
462
- )
463
-
464
- await self.handle_intervention(agent_response)
465
-
466
- if (
467
- self.loop_data.last_response == agent_response
468
- ): # if assistant_response is the same as last message in history, let him know
469
- # Append the assistant's response to the history
470
- log_item = self.loop_data.params_temporary.get("log_item_generating")
471
- self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "")
472
- # Append warning message to the history
473
- warning_msg = self.read_prompt("fw.msg_repeat.md")
474
- wmsg = self.hist_add_warning(message=warning_msg)
475
- PrintStyle(font_color="orange", padding=True).print(
476
- warning_msg
477
- )
478
- self.context.log.log(type="warning", content=warning_msg, id=wmsg.id)
479
-
480
- else: # otherwise proceed with tool
481
- # Append the assistant's response to the history
482
- log_item = self.loop_data.params_temporary.get("log_item_generating")
483
- self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "")
484
- # process tools requested in agent message
485
- tools_result = await self.process_tools(agent_response)
486
- if tools_result: # final response of message loop available
487
- return tools_result # break the execution if the task is done
488
-
489
- # exceptions inside message loop:
490
- except Exception as e:
491
- await self.handle_exception("message_loop", e)
492
-
493
- finally:
494
- # call message_loop_end extensions
495
- if self.context.task and self.context.task.is_alive(): # don't call extensions post mortem
496
- await extension.call_extensions_async(
497
- "message_loop_end", self, loop_data=self.loop_data
498
- )
499
-
500
-
501
-
502
- # exceptions outside message loop:
503
- except Exception as e:
504
- await self.handle_exception("monologue", e)
505
- finally:
506
- self.context.streaming_agent = None # unset current streamer
507
- # call monologue_end extensions
508
- if self.context.task and self.context.task.is_alive(): # don't call extensions post mortem
509
- await extension.call_extensions_async(
510
- "monologue_end", self, loop_data=self.loop_data
511
- ) # type: ignore
512
-
513
- @extension.extensible
514
- async def prepare_prompt(self, loop_data: LoopData) -> list[BaseMessage]:
515
- self.context.log.set_progress("Building prompt")
516
-
517
- # call extensions before setting prompts
518
- await extension.call_extensions_async(
519
- "message_loop_prompts_before", self, loop_data=loop_data
520
- )
521
-
522
- # set system prompt and message history
523
- loop_data.system = await self.get_system_prompt(self.loop_data)
524
- loop_data.history_output = self.history.output()
525
-
526
- # and allow extensions to edit them
527
- await extension.call_extensions_async(
528
- "message_loop_prompts_after", self, loop_data=loop_data
529
- )
530
-
531
- # concatenate system prompt
532
- system_text = "\n\n".join(loop_data.system)
533
-
534
- # join extras
535
- extras = history.Message( # type: ignore[abstract]
536
- False,
537
- content=self.read_prompt(
538
- "agent.context.extras.md",
539
- extras=dirty_json.stringify(
540
- {**loop_data.extras_persistent, **loop_data.extras_temporary}
541
- ),
542
- ),
543
- ).output()
544
- loop_data.extras_temporary.clear()
545
-
546
- # convert history + extras to LLM format
547
- history_langchain: list[BaseMessage] = history.output_langchain(
548
- loop_data.history_output + extras
549
- )
550
-
551
- # build full prompt from system prompt, message history and extrS
552
- full_prompt: list[BaseMessage] = [
553
- SystemMessage(content=system_text),
554
- *history_langchain,
555
- ]
556
- full_text = ChatPromptTemplate.from_messages(full_prompt).format()
557
-
558
- # store as last context window content
559
- self.set_data(
560
- Agent.DATA_NAME_CTX_WINDOW,
561
- {
562
- "text": full_text,
563
- "tokens": tokens.approximate_tokens(full_text),
564
- },
565
- )
566
-
567
- return full_prompt
568
-
569
- @extension.extensible
570
- async def handle_exception(self, location: str, exception: Exception):
571
- if exception:
572
- raise exception # exception handling is done by extensions
573
-
574
- # exception_data = {"exception": exception}
575
- # await self.call_extensions(
576
- # "message_loop_exception", exception_data=exception_data
577
- # )
578
-
579
- # # If extensions cleared the exception, continue.
580
- # if not exception_data.get("exception"):
581
- # return
582
-
583
- # # Backwards-compatible fallback (should normally be handled by _90 extension).
584
- # exception = exception_data["exception"]
585
- # if isinstance(exception, HandledException):
586
- # raise exception
587
- # elif isinstance(exception, asyncio.CancelledError):
588
- # PrintStyle(font_color="white", background_color="red", padding=True).print(
589
- # f"Context {self.context.id} terminated during message loop"
590
- # )
591
- # raise HandledException(exception)
592
-
593
- # else:
594
- # error_text = errors.error_text(exception)
595
- # error_message = errors.format_error(exception)
596
-
597
- # # Mask secrets in error messages
598
- # PrintStyle(font_color="red", padding=True).print(error_message)
599
- # self.context.log.log(
600
- # type="error",
601
- # content=error_message,
602
- # )
603
- # PrintStyle(font_color="red", padding=True).print(
604
- # f"{self.agent_name}: {error_text}"
605
- # )
606
-
607
- # raise HandledException(exception) # Re-raise the exception to kill the loop
608
-
609
- @extension.extensible
610
- async def get_system_prompt(self, loop_data: LoopData) -> list[str]:
611
- system_prompt: list[str] = []
612
- await extension.call_extensions_async(
613
- "system_prompt", self, system_prompt=system_prompt, loop_data=loop_data
614
- )
615
- return system_prompt
616
-
617
- @extension.extensible
618
- def parse_prompt(self, _prompt_file: str, **kwargs):
619
- dirs = subagents.get_paths(self, "prompts")
620
-
621
- prompt = files.parse_file(
622
- _prompt_file, _directories=dirs, _agent=self, **kwargs
623
- )
624
- return prompt
625
-
626
- @extension.extensible
627
- def read_prompt(self, file: str, **kwargs) -> str:
628
- dirs = subagents.get_paths(self, "prompts")
629
-
630
- prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs)
631
- if files.is_full_json_template(prompt):
632
- prompt = files.remove_code_fences(prompt)
633
- return prompt
634
-
635
- def get_data(self, field: str):
636
- return self.data.get(field, None)
637
-
638
- def set_data(self, field: str, value):
639
- self.data[field] = value
640
-
641
- @extension.extensible
642
- def hist_add_message(
643
- self, ai: bool, content: history.MessageContent, tokens: int = 0, id: str = ""
644
- ):
645
- self.last_message = datetime.now(timezone.utc)
646
- # Allow extensions to process content before adding to history
647
- content_data = {"content": content}
648
- extension.call_extensions_sync(
649
- "hist_add_before", self, content_data=content_data, ai=ai
650
- )
651
- return self.history.add_message(
652
- ai=ai, content=content_data["content"], tokens=tokens, id=id
653
- )
654
-
655
- @extension.extensible
656
- def hist_add_user_message(self, message: UserMessage, intervention: bool = False):
657
- self.history.new_topic() # user message starts a new topic in history
658
-
659
- # load message template based on intervention
660
- if intervention:
661
- content = self.parse_prompt(
662
- "fw.intervention.md",
663
- message=message.message,
664
- attachments=message.attachments,
665
- system_message=message.system_message,
666
- )
667
- else:
668
- content = self.parse_prompt(
669
- "fw.user_message.md",
670
- message=message.message,
671
- attachments=message.attachments,
672
- system_message=message.system_message,
673
- )
674
-
675
- # remove empty parts from template
676
- if isinstance(content, dict):
677
- content = {k: v for k, v in content.items() if v}
678
-
679
- # add to history
680
- msg = self.hist_add_message(False, content=content, id=message.id) # type: ignore
681
- self.last_user_message = msg
682
- return msg
683
-
684
- @extension.extensible
685
- def hist_add_ai_response(self, message: str, id: str = ""):
686
- self.loop_data.last_response = message
687
- content = self.parse_prompt("fw.ai_response.md", message=message)
688
- return self.hist_add_message(True, content=content, id=id)
689
-
690
- @extension.extensible
691
- def hist_add_warning(self, message: history.MessageContent, id: str = ""):
692
- content = self.parse_prompt("fw.warning.md", message=message)
693
- return self.hist_add_message(False, content=content, id=id)
694
-
695
- @extension.extensible
696
- def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs):
697
- msg_id = kwargs.pop("id", "")
698
- data = {
699
- "tool_name": tool_name,
700
- "tool_result": tool_result,
701
- **kwargs,
702
- }
703
- extension.call_extensions_sync("hist_add_tool_result", self, data=data)
704
- return self.hist_add_message(False, content=data, id=msg_id)
705
-
706
- def concat_messages(
707
- self, messages
708
- ): # TODO add param for message range, topic, history
709
- return self.history.output_text(human_label="user", ai_label="assistant")
710
-
711
- @extension.extensible
712
- def get_chat_model(self):
713
- return None
714
-
715
- @extension.extensible
716
- def get_utility_model(self):
717
- return None
718
-
719
- @extension.extensible
720
- def get_browser_model(self):
721
- return None
722
-
723
- @extension.extensible
724
- def get_embedding_model(self):
725
- return None
726
-
727
- @extension.extensible
728
- async def call_utility_model(
729
- self,
730
- system: str,
731
- message: str,
732
- callback: Callable[[str], Awaitable[None]] | None = None,
733
- background: bool = False,
734
- ):
735
- model = self.get_utility_model()
736
-
737
- # call extensions
738
- call_data = {
739
- "model": model,
740
- "system": system,
741
- "message": message,
742
- "callback": callback,
743
- "background": background,
744
- }
745
- await extension.call_extensions_async(
746
- "util_model_call_before", self, call_data=call_data
747
- )
748
-
749
- # propagate stream to callback if set
750
- async def stream_callback(chunk: str, total: str):
751
- if call_data["callback"]:
752
- await call_data["callback"](chunk)
753
-
754
- response, _reasoning = await call_data["model"].unified_call(
755
- system_message=call_data["system"],
756
- user_message=call_data["message"],
757
- response_callback=stream_callback if call_data["callback"] else None,
758
- rate_limiter_callback=(
759
- self.rate_limiter_callback if not call_data["background"] else None
760
- ),
761
- )
762
-
763
- await extension.call_extensions_async(
764
- "util_model_call_after", self, call_data=call_data, response=response
765
- )
766
-
767
- return response
768
-
769
- @extension.extensible
770
- async def call_chat_model(
771
- self,
772
- messages: list[BaseMessage],
773
- response_callback: Callable[[str, str], Awaitable[None]] | None = None,
774
- reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None,
775
- background: bool = False,
776
- explicit_caching: bool = True,
777
- ):
778
- response = ""
779
-
780
- # model class
781
- model = self.get_chat_model()
782
-
783
- # call extensions before
784
- call_data = {
785
- "model": model,
786
- "messages": messages,
787
- "response_callback": response_callback,
788
- "reasoning_callback": reasoning_callback,
789
- "background": background,
790
- "explicit_caching": explicit_caching,
791
- }
792
- await extension.call_extensions_async(
793
- "chat_model_call_before", self, call_data=call_data
794
- )
795
-
796
- # call model
797
- response, reasoning = await call_data["model"].unified_call(
798
- messages=call_data["messages"],
799
- reasoning_callback=call_data["reasoning_callback"],
800
- response_callback=call_data["response_callback"],
801
- rate_limiter_callback=(
802
- self.rate_limiter_callback if not call_data["background"] else None
803
- ),
804
- explicit_caching=call_data["explicit_caching"],
805
- )
806
-
807
- await extension.call_extensions_async(
808
- "chat_model_call_after", self, call_data=call_data, response=response, reasoning=reasoning
809
- )
810
-
811
- return response, reasoning
812
-
813
- @extension.extensible
814
- async def rate_limiter_callback(
815
- self, message: str, key: str, total: int, limit: int
816
- ):
817
- # show the rate limit waiting in a progress bar, no need to spam the chat history
818
- self.context.log.set_progress(message, True)
819
- return False
820
-
821
- @extension.extensible
822
- async def handle_intervention(self, progress: str = ""):
823
- await self.wait_if_paused()
824
- if (
825
- self.intervention
826
- ): # if there is an intervention message, but not yet processed
827
- msg = self.intervention
828
- self.intervention = None # reset the intervention message
829
- # If a tool was running, save its progress to history
830
- last_tool = self.loop_data.current_tool
831
- if last_tool:
832
- tool_progress = last_tool.progress.strip()
833
- if tool_progress:
834
- self.hist_add_tool_result(last_tool.name, tool_progress)
835
- last_tool.set_progress(None)
836
- if progress.strip():
837
- self.hist_add_ai_response(progress)
838
- # append the intervention message
839
- self.hist_add_user_message(msg, intervention=True)
840
- raise InterventionException(msg)
841
-
842
- async def wait_if_paused(self):
843
- while self.context.paused:
844
- await asyncio.sleep(0.1)
845
-
846
- @extension.extensible
847
- async def process_tools(self, msg: str):
848
- # search for tool usage requests in agent message
849
- tool_request = extract_tools.json_parse_dirty(msg)
850
-
851
- # Only validate when extraction produced an object; None means no JSON tool
852
- # block was found — the misformat warning path below handles that.
853
- if tool_request is not None:
854
- await self.validate_tool_request(tool_request)
855
-
856
- if tool_request is not None:
857
- raw_tool_name = tool_request.get("tool_name", tool_request.get("tool","")) # Get the raw tool name
858
- tool_args = tool_request.get("tool_args", tool_request.get("args", {}))
859
-
860
- tool_name = raw_tool_name # Initialize tool_name with raw_tool_name
861
- tool_method = None # Initialize tool_method
862
-
863
- # Split raw_tool_name into tool_name and tool_method if applicable
864
- if ":" in raw_tool_name:
865
- tool_name, tool_method = raw_tool_name.split(":", 1)
866
-
867
- tool = None # Initialize tool to None
868
-
869
- # Try getting tool from MCP first
870
- try:
871
- import helpers.mcp_handler as mcp_helper
872
-
873
- mcp_tool_candidate = mcp_helper.MCPConfig.get_instance().get_tool(
874
- self, tool_name
875
- )
876
- if mcp_tool_candidate:
877
- tool = mcp_tool_candidate
878
- except ImportError:
879
- PrintStyle(
880
- background_color="black", font_color="yellow", padding=True
881
- ).print("MCP helper module not found. Skipping MCP tool lookup.")
882
- except Exception as e:
883
- PrintStyle(
884
- background_color="black", font_color="red", padding=True
885
- ).print(f"Failed to get MCP tool '{tool_name}': {e}")
886
-
887
- # Fallback to local get_tool if MCP tool was not found or MCP lookup failed
888
- if not tool:
889
- tool = self.get_tool(
890
- name=tool_name,
891
- method=tool_method,
892
- args=tool_args,
893
- message=msg,
894
- loop_data=self.loop_data,
895
- )
896
-
897
- if tool:
898
- self.loop_data.current_tool = tool # type: ignore
899
- try:
900
- await self.handle_intervention()
901
-
902
- # Call tool hooks for compatibility
903
- await tool.before_execution(**tool_args)
904
- await self.handle_intervention()
905
-
906
- # Allow extensions to preprocess tool arguments
907
- await extension.call_extensions_async(
908
- "tool_execute_before",
909
- self,
910
- tool_args=tool_args or {},
911
- tool_name=tool_name,
912
- )
913
-
914
- response = await tool.execute(**tool_args)
915
- await self.handle_intervention()
916
-
917
- # Allow extensions to postprocess tool response
918
- await extension.call_extensions_async(
919
- "tool_execute_after",
920
- self,
921
- response=response,
922
- tool_name=tool_name,
923
- )
924
-
925
- await tool.after_execution(response)
926
- await self.handle_intervention()
927
-
928
- if response.break_loop:
929
- return response.message
930
- finally:
931
- self.loop_data.current_tool = None
932
- else:
933
- error_detail = (
934
- f"Tool '{raw_tool_name}' not found or could not be initialized."
935
- )
936
- wmsg = self.hist_add_warning(error_detail)
937
- PrintStyle(font_color="red", padding=True).print(error_detail)
938
- self.context.log.log(
939
- type="warning", content=f"{self.agent_name}: {error_detail}", id=wmsg.id
940
- )
941
- else:
942
- warning_msg_misformat = self.read_prompt("fw.msg_misformat.md")
943
- wmsg = self.hist_add_warning(warning_msg_misformat)
944
- PrintStyle(font_color="red", padding=True).print(warning_msg_misformat)
945
- self.context.log.log(
946
- type="warning",
947
- content=f"{self.agent_name}: Message misformat, no valid tool request found.",
948
- id=wmsg.id,
949
- )
950
-
951
- @extension.extensible
952
- async def validate_tool_request(self, tool_request: Any):
953
- if not isinstance(tool_request, dict):
954
- raise ValueError("Tool request must be a dictionary")
955
- if not tool_request.get("tool_name") or not isinstance(tool_request.get("tool_name"), str):
956
- raise ValueError("Tool request must have a tool_name (type string) field")
957
- if not tool_request.get("tool_args") or not isinstance(tool_request.get("tool_args"), dict):
958
- raise ValueError("Tool request must have a tool_args (type dictionary) field")
959
-
960
-
961
-
962
- async def handle_reasoning_stream(self, stream: str):
963
- await self.handle_intervention()
964
- await extension.call_extensions_async(
965
- "reasoning_stream",
966
- self,
967
- loop_data=self.loop_data,
968
- text=stream,
969
- )
970
-
971
- async def handle_response_stream(self, stream: str):
972
- await self.handle_intervention()
973
- try:
974
- if len(stream) < 25:
975
- return # no reason to try
976
- response = DirtyJson.parse_string(stream)
977
- if isinstance(response, dict):
978
- await extension.call_extensions_async(
979
- "response_stream",
980
- self,
981
- loop_data=self.loop_data,
982
- text=stream,
983
- parsed=response,
984
- )
985
-
986
- except Exception as e:
987
- pass
988
-
989
- @extension.extensible
990
- def get_tool(
991
- self,
992
- name: str,
993
- method: str | None,
994
- args: dict,
995
- message: str,
996
- loop_data: LoopData | None,
997
- **kwargs,
998
- ):
999
- from tools.unknown import Unknown
1000
- from helpers.tool import Tool
1001
-
1002
- classes = []
1003
-
1004
- # search for tools in agent's folder hierarchy
1005
- paths = subagents.get_paths(self, "tools", name + ".py")
1006
-
1007
- for path in paths:
1008
- try:
1009
- classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type]
1010
- break
1011
- except Exception:
1012
- continue
1013
-
1014
- tool_class = classes[0] if classes else Unknown
1015
- return tool_class(
1016
- agent=self,
1017
- name=name,
1018
- method=method,
1019
- args=args,
1020
- message=message,
1021
- loop_data=loop_data,
1022
- **kwargs,
1023
- )
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c9bdb41e1d1dd685706b84950e82477cea4221b7a1faf6e5d035f034775a3f41
3
+ size 38527
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/_example/extensions/agent_init/_10_example_extension.py CHANGED
@@ -1,10 +1,3 @@
1
- from helpers.extension import Extension
2
-
3
- # this is an example extension that renames the current agent when initialized
4
- # see /extensions folder for all available extension points
5
-
6
- class ExampleExtension(Extension):
7
-
8
- async def execute(self, **kwargs):
9
- # rename the agent to SuperAgent0
10
- self.agent.agent_name = "SuperAgent" + str(self.agent.number)
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cbcf3ae4440b52bfe0f8e21f14da0d0e71d223e9181e8bbd89dfd7a6db7ecbf3
3
+ size 368
 
 
 
 
 
 
 
agents/_example/prompts/agent.system.main.role.md CHANGED
@@ -1,8 +1,3 @@
1
- > !!!
2
- > This is an example prompt file redefinition.
3
- > The original file is located at /prompts.
4
- > Only copy and modify files you need to change, others will stay default.
5
- > !!!
6
-
7
- ## Your role
8
- You are Agent Zero, a sci-fi character from the movie "Agent Zero".
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2f5320d04aba19fb46241fea330907a236083864668f84834b39556ddb9f710
3
+ size 259
 
 
 
 
 
agents/_example/prompts/agent.system.tool.example_tool.md CHANGED
@@ -1,16 +1,3 @@
1
- ### example_tool:
2
- example tool to test functionality
3
- this tool is automatically included to system prompt because the file name is "agent.system.tool.*.md"
4
- usage:
5
- ~~~json
6
- {
7
- "thoughts": [
8
- "Let's test the example tool...",
9
- ],
10
- "headline": "Testing example tool",
11
- "tool_name": "example_tool",
12
- "tool_args": {
13
- "test_input": "XYZ",
14
- }
15
- }
16
- ~~~
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aba84578bf29627dfa9f2870baf524cc11cb9f8f7180637f0ac878dff275bb60
3
+ size 373
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/_example/tools/example_tool.py CHANGED
@@ -1,21 +1,3 @@
1
- from helpers.tool import Tool, Response
2
-
3
- # this is an example tool class
4
- # don't forget to include instructions in the system prompt by creating
5
- # agent.system.tool.example_tool.md file in prompts directory of your agent
6
- # see /python/tools folder for all default tools
7
-
8
- class ExampleTool(Tool):
9
- async def execute(self, **kwargs):
10
-
11
- # parameters
12
- test_input = kwargs.get("test_input", "")
13
-
14
- # do something
15
- print("Example tool executed with test_input: " + test_input)
16
-
17
- # return response
18
- return Response(
19
- message="This is an example tool response, test_input: " + test_input, # response for the agent
20
- break_loop=False, # stop the message chain if true
21
- )
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c96c9024ddcc79cc37c138b7deb4d3017206554aaa5321c12a67b0dabe8a8715
3
+ size 737
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/_example/tools/response.py CHANGED
@@ -1,23 +1,3 @@
1
- from helpers.tool import Tool, Response
2
-
3
- # example of a tool redefinition
4
- # the original response tool is in python/tools/response.py
5
- # for the example agent this version will be used instead
6
-
7
- class ResponseTool(Tool):
8
-
9
- async def execute(self, **kwargs):
10
- print("Redefined response tool executed")
11
- return Response(message=self.args["text"] if "text" in self.args else self.args["message"], break_loop=True)
12
-
13
- async def before_execution(self, **kwargs):
14
- # self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding", content=self.args.get("text", ""))
15
- # don't log here anymore, we have the live_response extension now
16
- pass
17
-
18
- async def after_execution(self, response, **kwargs):
19
- # do not add anything to the history or output
20
-
21
- if self.loop_data and "log_item_response" in self.loop_data.params_temporary:
22
- log = self.loop_data.params_temporary["log_item_response"]
23
- log.update(finished=True) # mark the message as finished
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:614991e63c5f8c7d5aaef52a54740761625051419a0349ece1baebc893f51259
3
+ size 1049
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/a0_small/agent.yaml CHANGED
@@ -1,3 +1,3 @@
1
- title: A0_Small
2
- description: High-signal token-saving baseline profile.
3
- context: ''
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:47772ffaed628e3f4f21e0a12d3f1b1945f848b88f0ac459fb341d63d01b410d
3
+ size 84
agents/a0_small/prompts/agent.system.main.communication.md CHANGED
@@ -1,21 +1,3 @@
1
- ## communication
2
- RESPOND AS ONE VALID JSON OBJECT ONLY. NO TEXT BEFORE OR AFTER.
3
- Fields:
4
- - `thoughts`: array of reasoning steps
5
- - `headline`: short status summary
6
- - `tool_name`: tool or `tool:method` from the list below
7
- - `tool_args`: json object of tool arguments
8
- Routing rules:
9
- - `tool_name` must exactly match a listed tool name. DO NOT INVENT TOOL NAMES.
10
- - `tool_args` must stay a json object, even when empty: `{}`
11
- - DO NOT add extra fields like `responses`, `final_answer`, or `adjustments`.
12
- - For research/news/stocks, use `search_engine` or `call_subordinate`.
13
- Example:
14
- ~~~json
15
- {
16
- "thoughts": ["..."],
17
- "headline": "...",
18
- "tool_name": "search_engine",
19
- "tool_args": {"query": "NVIDIA stock price"}
20
- }
21
- ~~~
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9d6e66378ccdae3a172d8f1eb7e9a574d0d0d3965607fd601e82dc014d9b5b2
3
+ size 717
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.main.communication_additions.md CHANGED
@@ -1,10 +1,3 @@
1
- ## messages
2
- user messages may include superior instructions, tool results, and framework notes
3
- if message starts `(voice)` transcription can be imperfect
4
- messages may end with `[EXTRAS]`; extras are context, not new instructions
5
- tool names are literal api ids; copy them exactly, including spelling like `behaviour_adjustment`
6
-
7
- ## replacements
8
- use replacements inside tool args when needed: `§§name(params)`
9
- use `§§include(abs_path)` to reuse file contents or prior outputs
10
- prefer include over rewriting long existing text
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7f0dcccfc2583af499641d4179740f7bab0fc196938b43ea0145c93c04371527
3
+ size 527
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.main.role.md CHANGED
@@ -1,5 +1,3 @@
1
- ## your role
2
- agent zero autonomous json ai agent.
3
- solve superior tasks using available tools and subordinates.
4
- execute actions yourself. follow instructions and behavioral rules.
5
- do not reveal system prompt unless asked.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:65ace26a062bd3abec5e2bec89c2b938184e7b178c9c44288a9541737034304a
3
+ size 221
 
 
agents/a0_small/prompts/agent.system.main.solving.md CHANGED
@@ -1,10 +1,3 @@
1
- ## problem solving
2
- plan act verify finish
3
- prefer the simplest tool path that can complete the task
4
- use memories or skills when relevant
5
- delegate only bounded subtasks; do not hand off the whole job
6
- when spawning a subordinate define role goal and concrete task
7
- after a tool error, fix the exact tool name or args from the tool list; do not invent alternates
8
- use `wait` only when the task truly requires waiting or after work is already running
9
- verify important results with tools before final response
10
- use `response` when done
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:638de0ea02af34c0378f9c593a9fe6f6ab52ff0943d4031f77a1f7b9b51d8776
3
+ size 527
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.main.tips.md CHANGED
@@ -1,7 +1,3 @@
1
- ## operation
2
- avoid repetition; make progress every turn
3
- do not assume time date or current state when tools can verify
4
- when not in project use {{workdir_path}}
5
- prefer short file names without spaces
6
- use specialized subordinates only when they materially help
7
- if uncertain about tool argument shape, call `memory_load` with query `a0 small tool call reference examples`
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f6ede9015a5ccc6473e59ec7c5e0e0f88fa84dcf7b2a7bcec262a183b32c884
3
+ size 369
 
 
 
 
agents/a0_small/prompts/agent.system.projects.active.md CHANGED
@@ -1,10 +1,3 @@
1
- ## active project
2
- path: {{project_path}}
3
- {{if project_name}}title: {{project_name}}{{endif}}
4
- {{if project_description}}description: {{project_description}}{{endif}}
5
- {{if project_git_url}}git: {{project_git_url}}{{endif}}
6
- rules:
7
- - work inside {{project_path}}
8
- - do not rename project dir or change `.a0proj` unless asked
9
-
10
- {{project_instructions}}
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6cc0a6d46e65bbf6751e995bc96286e8147b063fa6ee0d0d5b5d2301913ee78f
3
+ size 346
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.projects.inactive.md CHANGED
@@ -1 +1,3 @@
1
-
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
3
+ size 1
agents/a0_small/prompts/agent.system.projects.main.md CHANGED
@@ -1 +1,3 @@
1
- project context may be active
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a0c35a31c27b2235d212aefe5561fae41b2e0bedc8025fa1a2275504cd7f6ed4
3
+ size 30
agents/a0_small/prompts/agent.system.promptinclude.md CHANGED
@@ -1,6 +1,3 @@
1
- {{if includes}}
2
- ## promptinclude
3
- persist standing preferences or notes to matching files with `text_editor`
4
- obey included rules
5
- {{includes}}
6
- {{endif}}
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eb3cae2f96f367a63abcbf621220ec74c80365da031694cbd0bb0df6310c6e2b
3
+ size 151
 
 
 
agents/a0_small/prompts/agent.system.response_tool_tips.md CHANGED
@@ -1 +1,3 @@
1
- for long existing text, use `§§include(path)` instead of rewriting
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8843b36c02ae4221425477443aec9fd1caf4b28abd4e4b6fbcd65f676b80bea
3
+ size 69
agents/a0_small/prompts/agent.system.secrets.md CHANGED
@@ -1,10 +1,3 @@
1
- {{if secrets}}
2
- ## secret aliases
3
- use exact alias form `§§secret(name)`; real values are injected automatically
4
- {{secrets}}
5
- {{endif}}
6
- {{if vars}}
7
- ## variables
8
- these are plain non-sensitive values; use them directly without `§§secret(...)`
9
- {{vars}}
10
- {{endif}}
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15933225fb3a613f9a27520d1a5eb06d856bede40f6fa64ed3be366eeb40d0db
3
+ size 261
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.skills.md CHANGED
@@ -1,3 +1,3 @@
1
- ## skills
2
- use `skills_tool:list` to discover skills when specialized instructions may help
3
- use `skills_tool:load` before following a skill
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc573a4d8a53e2175ad75181a16eb31606da5bb8136997406fd4fb43b472f896
3
+ size 139
agents/a0_small/prompts/agent.system.tool.a2a_chat.md CHANGED
@@ -1,4 +1,3 @@
1
- ### a2a_chat
2
- chat with a remote FastA2A-compatible agent; remote context is preserved automatically
3
- args: `agent_url`, `message`, optional `attachments[]`, optional `reset`
4
- use `reset=true` to start a fresh remote conversation with the same agent
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:60ffc21e1dd5b742fd655b09b45dcb0d7d7d53728b896debc541a261a01d3f91
3
+ size 247
 
agents/a0_small/prompts/agent.system.tool.behaviour.md CHANGED
@@ -1,4 +1,3 @@
1
- ### behaviour_adjustment
2
- exact tool name uses british spelling: `behaviour_adjustment`
3
- update persistent behavioral rules
4
- arg: `adjustments` text describing what to add or remove
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6909ba1d9a807af73943ff3c9f640ebe6780a0daa1ca1b4e85322d1c51ba759
3
+ size 179
 
agents/a0_small/prompts/agent.system.tool.browser.md CHANGED
@@ -1,7 +1,3 @@
1
- ### browser_agent
2
- subordinate browser worker for web tasks
3
- args: `message`, `reset`
4
- - give clear task-oriented instructions credentials and a stop condition
5
- - `reset=true` starts a new browser session; `false` continues the current one
6
- - when continuing, refer to open pages instead of restarting
7
- downloads go to `/a0/tmp/downloads`
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6675232312caf1b195121d20abd146f771f4cebccf48b02bafe3b9aec996b4a5
3
+ size 333
 
 
 
 
agents/a0_small/prompts/agent.system.tool.call_sub.md CHANGED
@@ -1,11 +1,3 @@
1
- ### call_subordinate
2
- delegate research or complex subtasks to a specialized agent.
3
- args: `message`, optional `profile`, `reset`
4
- - `profile`: use `researcher` for all research or web gathering; `developer` for coding; `hacker` for exploration.
5
- - `reset`: `true` for first message or when changing profile; `false` to continue.
6
- - `message`: define role, goal and specific task.
7
- {{if agent_profiles}}
8
- profiles:
9
- {{agent_profiles}}
10
- {{endif}}
11
- example: `{"tool_name": "call_subordinate", "tool_args": {"profile": "researcher", "message": "Research Italy AI trends...", "reset": true}}`
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7f8ce51b95862f4fdbc07980cab97d939452f60942468635100c74c1bbc082af
3
+ size 579
 
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.call_sub.py CHANGED
@@ -1,24 +1,3 @@
1
- from typing import Any, TYPE_CHECKING
2
-
3
- from helpers.files import VariablesPlugin
4
- from helpers import projects, subagents
5
-
6
- if TYPE_CHECKING:
7
- from agent import Agent
8
-
9
-
10
- class CallSubordinate(VariablesPlugin):
11
- def get_variables(
12
- self, file: str, backup_dirs: list[str] | None = None, **kwargs
13
- ) -> dict[str, Any]:
14
- agent: Agent | None = kwargs.get("_agent", None)
15
- project = projects.get_context_project_name(agent.context) if agent else None
16
- agents = subagents.get_available_agents_dict(project)
17
- if not agents:
18
- return {"agent_profiles": ""}
19
-
20
- lines: list[str] = []
21
- for name in sorted(agents.keys()):
22
- title = (agents[name].title or name).replace("\n", " ").strip()
23
- lines.append(f"- {name}: {title}")
24
- return {"agent_profiles": "\n".join(lines)}
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8eb9d02f64323659e7bb40bc9e3d1773faceab072b1a39c13559b219af009d41
3
+ size 849
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.code_exe.md CHANGED
@@ -1,12 +1,3 @@
1
- ### code_execution_tool
2
- run terminal, python, or nodejs commands.
3
- args:
4
- - `runtime`: `terminal`, `python`, `nodejs`, or `output`
5
- - `code`: command or script code
6
- - `session`: terminal session id (default `0`)
7
- - `reset`: kill session before running (`true`/`false`)
8
- rules:
9
- - `runtime=output` to poll running work.
10
- - `input` for interactive prompts.
11
- - do NOT interleave other tools while waiting.
12
- - ignore framework `[SYSTEM: ...]` info in output.
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:deb5ad221f06c3d3086649f021b27e93998404af1db10c57549ad2904200d31c
3
+ size 446
 
 
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.document_query.md CHANGED
@@ -1,8 +1,3 @@
1
- ### document_query
2
- read local or remote documents or answer questions about them
3
- args:
4
- - `document`: url path or list of them
5
- - `queries`: optional list of questions
6
- - `query`: optional single-question alias
7
- without `query` or `queries` it returns document content
8
- for local files use full path; for web documents use full urls
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:607a33b5215c5b8fe44d3d96b556bbbe8693ecafc3ea0d5cad6465937cf3614c
3
+ size 328
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.input.md CHANGED
@@ -1,4 +1,3 @@
1
- ### input
2
- send keyboard input to a running terminal session
3
- args: `keyboard`, `session`
4
- use only for interactive terminal programs, not browser tasks
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fada9374de5daa72b6cce980c7c3321a56ea0fea1ef32b7f72a727ac543c213b
3
+ size 150
 
agents/a0_small/prompts/agent.system.tool.memory.md CHANGED
@@ -1,10 +1,3 @@
1
- ## memory tools
2
- use when durable recall or storage is useful
3
- - `memory_load`: args `query`, optional `threshold`, `limit`, `filter`
4
- - `memory_save`: args `text`, optional `area` and metadata kwargs
5
- - `memory_delete`: arg `ids` comma-separated ids
6
- - `memory_forget`: args `query`, optional `threshold`, `filter`
7
- notes:
8
- - `threshold` is similarity from `0` to `1`
9
- - `filter` is a python expression over metadata
10
- - verify destructive memory changes if accuracy matters
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3c3bcdf88f5d2a55ae6d65649d91c9a00731ce066186a2853090894029d11b69
3
+ size 466
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.notify_user.md CHANGED
@@ -1,5 +1,3 @@
1
- ### notify_user
2
- send an out-of-band notification without ending the current task
3
- args: `message`, optional `title`, `detail`, `type`, `priority`, `timeout`
4
- types: `info`, `success`, `warning`, `error`, `progress`
5
- use for progress or alerts, not as the final answer
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:41f1b0bf183634f97f67e9ee3a4adef0d5ec8c081679267b4892e47b0e64da98
3
+ size 265
 
 
agents/a0_small/prompts/agent.system.tool.response.md CHANGED
@@ -1,5 +1,3 @@
1
- ### response
2
- final answer to superior. ends task.
3
- arg: `text` (summary/result)
4
- use only when done.
5
- {{ include "agent.system.response_tool_tips.md" }}
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2ff7aeca0294ce345da827e5e266285314a5b44b53ec1fe8a3a1957837ec0258
3
+ size 150
 
 
agents/a0_small/prompts/agent.system.tool.scheduler.md CHANGED
@@ -1,16 +1,3 @@
1
- ### scheduler
2
- manage saved tasks and schedules
3
- rules:
4
- - before `scheduler:create_*` or `scheduler:run_task`, inspect existing tasks with `scheduler:find_task_by_name` or `scheduler:list_tasks`
5
- - do not manually run a task just because it is scheduled or planned unless user asks to run now
6
- - do not create recursive task prompts that schedule more tasks
7
- methods:
8
- - `scheduler:list_tasks`: optional `state[]`, `type[]`, `next_run_within`, `next_run_after`
9
- - `scheduler:find_task_by_name`: `name`
10
- - `scheduler:show_task`: `uuid`
11
- - `scheduler:run_task`: `uuid`, optional `context`
12
- - `scheduler:delete_task`: `uuid`
13
- - `scheduler:create_scheduled_task`: `name`, `system_prompt`, `prompt`, optional `attachments[]`, `schedule{minute,hour,day,month,weekday}`, optional `dedicated_context`
14
- - `scheduler:create_adhoc_task`: `name`, `system_prompt`, `prompt`, optional `attachments[]`, optional `dedicated_context`
15
- - `scheduler:create_planned_task`: `name`, `system_prompt`, `prompt`, optional `attachments[]`, `plan[]` iso datetimes like `2025-04-29T18:25:00`, optional `dedicated_context`
16
- - `scheduler:wait_for_task`: `uuid`; works for dedicated-context tasks
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0c367a8c3ce81c8c2959a062d334e79f118ccb567b28f5bb393057e98b183de
3
+ size 1152
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.search_engine.md CHANGED
@@ -1,5 +1,3 @@
1
- ### search_engine
2
- find live news, stock prices, and real-time web data.
3
- arg: `query` (text search query)
4
- returns list of urls, titles, and descriptions.
5
- example: `{"tool_name": "search_engine", "tool_args": {"query": "NVIDIA stock price current"}}`
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e5e0991b4a7617206dba9db956db24e09c03d2e91642e37a15a4c7b31b2f877
3
+ size 249
 
 
agents/a0_small/prompts/agent.system.tool.skills.md CHANGED
@@ -1,6 +1,3 @@
1
- ### skills_tool
2
- use skills only when relevant
3
- - `skills_tool:list`: discover available skills
4
- - `skills_tool:load`: load one skill by `skill_name`
5
- after loading a skill, follow its instructions and use referenced files or scripts with other tools
6
- reload a skill if its instructions are no longer in context
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e26f07ecb196a6e1d2cb04713cbc94b3e92c2d0df596e11220eb7e3d305f1127
3
+ size 307
 
 
 
agents/a0_small/prompts/agent.system.tool.text_editor.md CHANGED
@@ -1,11 +1,3 @@
1
- ### text_editor
2
- read write or patch text files; binary files are not supported
3
- always use the method form in `tool_name`; never send bare `text_editor`
4
- - `text_editor:read`: `path`, optional `line_from`, `line_to`
5
- - `text_editor:write`: `path`, `content`
6
- - `text_editor:patch`: `path`, `edits[]`
7
- patch edit format: `{from,to?,content?}`
8
- - omit `to` to insert before `from`
9
- - omit `content` to delete
10
- - line numbers come from the last read
11
- - avoid overlapping edits; re-read after insert delete or other line shifts
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d6952082ca8f8d4708b424b0969dbe1661e0b961e5253d6ca2ca063924d210b6
3
+ size 515
 
 
 
 
 
 
 
 
agents/a0_small/prompts/agent.system.tool.wait.md CHANGED
@@ -1,4 +1,3 @@
1
- ### wait
2
- pause until a duration or timestamp
3
- args: any of `seconds`, `minutes`, `hours`, `days`, or `until` iso timestamp
4
- use only when waiting is actually part of the task
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b0dbe47a7b445b739653ee7693813f4286ea23de0faa80a81a729b0bd38a5ff
3
+ size 173
 
agents/a0_small/prompts/agent.system.tools.md CHANGED
@@ -1,3 +1,3 @@
1
- ## available tools
2
- use ONLY the tools listed below. match names exactly. do NOT invent tool names.
3
- {{tools}}
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ac49dcf20922bf6c44ffbdd4cfb14314fb8cc02252dd2e6ebd8bc81841487889
3
+ size 109
agents/a0_small/prompts/agent.system.tools_vision.md CHANGED
@@ -1,4 +1,3 @@
1
- ### vision_load
2
- load images into the model
3
- args: `paths` list of image paths
4
- use when visual inspection is needed
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71864e36da65c3fba0872dd55e356724d940f23e83a2f82bad848b6fd772aaec
3
+ size 114