jayc1978 commited on
Commit
8a87cbb
·
verified ·
1 Parent(s): 2ed4613

Upload initial Aegis project

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +33 -0
  2. .mypy_cache/.gitignore +2 -0
  3. .mypy_cache/CACHEDIR.TAG +3 -0
  4. .pytest_cache/.gitignore +2 -0
  5. .pytest_cache/CACHEDIR.TAG +4 -0
  6. .pytest_cache/README.md +8 -0
  7. .pytest_cache/v/cache/nodeids +58 -0
  8. .ruff_cache/.gitignore +2 -0
  9. .ruff_cache/0.15.2/12944381863948167845 +0 -0
  10. .ruff_cache/0.15.2/9042146699542604200 +0 -0
  11. .ruff_cache/0.16.0/3506816079193222284 +0 -0
  12. .ruff_cache/0.16.0/6668019937987631587 +0 -0
  13. .ruff_cache/0.16.0/9602145004845211386 +0 -0
  14. .ruff_cache/CACHEDIR.TAG +1 -0
  15. AGENTS.md +0 -0
  16. API.md +0 -0
  17. ARCHITECTURE.md +0 -0
  18. CHANGELOG.md +0 -0
  19. CONTRIBUTING.md +0 -0
  20. LICENSE +0 -0
  21. README.md +101 -0
  22. ROADMAP.md +0 -0
  23. SECURITY.md +0 -0
  24. TESTING.md +0 -0
  25. pyproject.toml +51 -0
  26. src/aegis/__init__.py +11 -0
  27. src/aegis/agents/__init__.py +10 -0
  28. src/aegis/agents/evaluator.py +66 -0
  29. src/aegis/agents/planner.py +50 -0
  30. src/aegis/cli/__init__.py +3 -0
  31. src/aegis/cli/app.py +37 -0
  32. src/aegis/cli/commands/__init__.py +0 -0
  33. src/aegis/cli/commands/run.py +193 -0
  34. src/aegis/cli/output.py +45 -0
  35. src/aegis/core/__init__.py +41 -0
  36. src/aegis/core/conductor.py +22 -0
  37. src/aegis/core/contracts.py +31 -0
  38. src/aegis/core/memory.py +85 -0
  39. src/aegis/core/models.py +44 -0
  40. src/aegis/core/policies.py +46 -0
  41. src/aegis/core/registry.py +54 -0
  42. src/aegis/core/tool_registry.py +54 -0
  43. src/aegis/core/tool_runner.py +16 -0
  44. src/aegis/core/workflow.py +101 -0
  45. src/aegis/evaluation/__init__.py +0 -0
  46. src/aegis/memory/__init__.py +0 -0
  47. src/aegis/observability/__init__.py +0 -0
  48. src/aegis/providers/__init__.py +0 -0
  49. src/aegis/runtime/__init__.py +27 -0
  50. src/aegis/runtime/errors.py +37 -0
.gitignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .coverage
9
+ htmlcov/
10
+
11
+ # Environments
12
+ .venv/
13
+ venv/
14
+ .env
15
+ .env.*
16
+
17
+ # Build
18
+ build/
19
+ dist/
20
+
21
+ # Editors and OS
22
+ .vscode/
23
+ .idea/
24
+ .DS_Store
25
+
26
+ # Runtime artifacts
27
+ logs/
28
+ runs/
29
+ artifacts/
30
+ *.log
31
+
32
+ __pycache__/
33
+ *.py[cod]
.mypy_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Automatically created by mypy
2
+ *
.mypy_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag automatically created by mypy.
3
+ # For information about cache directory tags see https://bford.info/cachedir/
.pytest_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Created by pytest automatically.
2
+ *
.pytest_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
.pytest_cache/v/cache/nodeids ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "tests/unit/test_cli_run.py::test_main_supports_help_and_execution[argv0-0]",
3
+ "tests/unit/test_cli_run.py::test_main_supports_help_and_execution[argv1-0]",
4
+ "tests/unit/test_cli_run.py::test_run_command_evaluator_failure",
5
+ "tests/unit/test_cli_run.py::test_run_command_json_output_failure",
6
+ "tests/unit/test_cli_run.py::test_run_command_json_output_success",
7
+ "tests/unit/test_cli_run.py::test_run_command_max_steps_forwarded",
8
+ "tests/unit/test_cli_run.py::test_run_command_rejects_empty_task",
9
+ "tests/unit/test_cli_run.py::test_run_command_runtime_exception",
10
+ "tests/unit/test_cli_run.py::test_run_command_success_with_text_output",
11
+ "tests/unit/test_cli_run.py::test_run_command_unknown_agent",
12
+ "tests/unit/test_cli_run.py::test_run_command_unknown_workflow",
13
+ "tests/unit/test_cli_run.py::test_run_command_verbose_failure_has_no_stack_trace_for_known_error",
14
+ "tests/unit/test_conductor.py::test_conductor_executes_registered_agent",
15
+ "tests/unit/test_conductor.py::test_conductor_rejects_mismatched_result_name",
16
+ "tests/unit/test_conductor.py::test_conductor_rejects_missing_agent",
17
+ "tests/unit/test_evaluator.py::test_evaluator_fails_unsuccessful_result",
18
+ "tests/unit/test_evaluator.py::test_evaluator_passes_valid_result",
19
+ "tests/unit/test_evaluator.py::test_evaluator_rejects_invalid_result",
20
+ "tests/unit/test_evaluator.py::test_evaluator_rejects_missing_result",
21
+ "tests/unit/test_memory.py::test_memory_store_adds_and_gets_entry",
22
+ "tests/unit/test_memory.py::test_memory_store_clear_removes_all_entries",
23
+ "tests/unit/test_memory.py::test_memory_store_deletes_entry",
24
+ "tests/unit/test_memory.py::test_memory_store_initializes_with_entries",
25
+ "tests/unit/test_memory.py::test_memory_store_normalizes_keys",
26
+ "tests/unit/test_memory.py::test_memory_store_raises_for_missing_key",
27
+ "tests/unit/test_memory.py::test_memory_store_rejects_delete_for_missing_key",
28
+ "tests/unit/test_memory.py::test_memory_store_rejects_duplicate_key",
29
+ "tests/unit/test_memory.py::test_memory_store_rejects_empty_key_lookup",
30
+ "tests/unit/test_memory.py::test_memory_store_set_replaces_existing_entry",
31
+ "tests/unit/test_models.py::test_agent_context_contains_task",
32
+ "tests/unit/test_models.py::test_agent_result_success",
33
+ "tests/unit/test_models.py::test_task_defaults_to_pending",
34
+ "tests/unit/test_planner.py::test_planner_output_is_bounded",
35
+ "tests/unit/test_planner.py::test_planner_returns_structured_plan",
36
+ "tests/unit/test_planner.py::test_planner_steps_have_sequential_ids",
37
+ "tests/unit/test_policies.py::test_agent_policy_combines_response_and_execution_rules",
38
+ "tests/unit/test_policies.py::test_execution_policy_defaults_to_restricted_permissions",
39
+ "tests/unit/test_policies.py::test_invalid_policy_limit_is_rejected",
40
+ "tests/unit/test_policies.py::test_response_policy_defaults",
41
+ "tests/unit/test_registry.py::test_registry_raises_when_agent_is_missing",
42
+ "tests/unit/test_registry.py::test_registry_registers_and_resolves_agent",
43
+ "tests/unit/test_registry.py::test_registry_rejects_duplicate_agent_names",
44
+ "tests/unit/test_registry.py::test_registry_returns_sorted_names",
45
+ "tests/unit/test_tool_registry.py::test_registry_accepts_initial_tools",
46
+ "tests/unit/test_tool_registry.py::test_registry_normalizes_tool_names",
47
+ "tests/unit/test_tool_registry.py::test_registry_raises_when_tool_is_missing",
48
+ "tests/unit/test_tool_registry.py::test_registry_registers_and_resolves_tool",
49
+ "tests/unit/test_tool_registry.py::test_registry_rejects_duplicate_tool_names",
50
+ "tests/unit/test_tool_registry.py::test_registry_rejects_empty_tool_name",
51
+ "tests/unit/test_tool_registry.py::test_registry_returns_sorted_names",
52
+ "tests/unit/test_tool_runner.py::test_tool_runner_executes_registered_tool",
53
+ "tests/unit/test_tool_runner.py::test_tool_runner_preserves_arguments",
54
+ "tests/unit/test_workflow.py::test_workflow_evaluates_failed_execution",
55
+ "tests/unit/test_workflow.py::test_workflow_runs_plan_execution_and_evaluation",
56
+ "tests/unit/test_workflow.py::test_workflow_stops_when_planner_fails",
57
+ "tests/unit/test_workflow.py::test_workflow_stores_all_results_in_memory"
58
+ ]
.ruff_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Automatically created by ruff.
2
+ *
.ruff_cache/0.15.2/12944381863948167845 ADDED
Binary file (469 Bytes). View file
 
.ruff_cache/0.15.2/9042146699542604200 ADDED
Binary file (190 Bytes). View file
 
.ruff_cache/0.16.0/3506816079193222284 ADDED
Binary file (1.62 kB). View file
 
.ruff_cache/0.16.0/6668019937987631587 ADDED
Binary file (492 Bytes). View file
 
.ruff_cache/0.16.0/9602145004845211386 ADDED
Binary file (884 Bytes). View file
 
.ruff_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1 @@
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
AGENTS.md ADDED
File without changes
API.md ADDED
File without changes
ARCHITECTURE.md ADDED
File without changes
CHANGELOG.md ADDED
File without changes
CONTRIBUTING.md ADDED
File without changes
LICENSE ADDED
File without changes
README.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Aegis
2
+
3
+ Aegis is a modular Python framework for composing agent workflows, registries, memory, tool execution, and evaluation. The repository now ships a runnable CLI that executes tasks through the built-in orchestration layer.
4
+
5
+ ## What it does now
6
+
7
+ - Registers agents and tools through typed registries
8
+ - Executes workflows through a conductor and workflow engine
9
+ - Stores run-scoped results in an in-memory memory store
10
+ - Evaluates run output with a deterministic evaluator
11
+ - Exposes a user-facing CLI: `aegis run`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ cd /Users/giancarlovizhnay/aegis
17
+ python -m venv .venv
18
+ source .venv/bin/activate
19
+ pip install -e ".[dev]"
20
+ aegis run --help
21
+ aegis run "Run the default Aegis workflow"
22
+ ```
23
+
24
+ ## CLI usage
25
+
26
+ Default text output:
27
+
28
+ ```bash
29
+ aegis run "Review this repository"
30
+ ```
31
+
32
+ Run a specific agent:
33
+
34
+ ```bash
35
+ aegis run --agent default-task "Review this repository"
36
+ ```
37
+
38
+ Run a named workflow:
39
+
40
+ ```bash
41
+ aegis run --workflow review "Review this repository"
42
+ ```
43
+
44
+ JSON output:
45
+
46
+ ```bash
47
+ aegis run --output json "Review this repository"
48
+ ```
49
+
50
+ Limit execution steps:
51
+
52
+ ```bash
53
+ aegis run --max-steps 10 "Review this repository"
54
+ ```
55
+
56
+ ## Available options
57
+
58
+ - `--agent <name>`: run a specific registered agent
59
+ - `--workflow <name>`: run a named workflow
60
+ - `--output text|json`: choose human-readable or structured output
61
+ - `--max-steps <n>`: pass a step limit into the runtime context
62
+ - `--verbose`: show debug details for unexpected failures only
63
+
64
+ ## Example output
65
+
66
+ ```text
67
+ Run ID: run_...
68
+ Status: completed
69
+ Agent: default-task
70
+ Workflow: default
71
+ Result:
72
+ {
73
+ "summary": "Received task: Review this repository"
74
+ }
75
+ ```
76
+
77
+ ## Exit codes
78
+
79
+ - `0`: success
80
+ - `1`: unexpected internal error
81
+ - `2`: invalid CLI input
82
+ - `3`: configuration or registry error
83
+ - `4`: policy violation
84
+ - `5`: execution failure
85
+ - `6`: evaluation failure
86
+
87
+ ## Current limitations
88
+
89
+ - Persistence is still in-memory and resets on restart.
90
+ - The default workflow is intentionally small and deterministic.
91
+ - No external model provider is wired in yet.
92
+ - No HTTP API is exposed yet.
93
+ - Tool integrations are still skeletal.
94
+
95
+ ## End-to-end example
96
+
97
+ ```bash
98
+ aegis run --workflow review --output json "Review this repository"
99
+ ```
100
+
101
+ This returns a structured JSON report with the run ID, selected workflow, execution result, evaluation payload, and any error information.
ROADMAP.md ADDED
File without changes
SECURITY.md ADDED
File without changes
TESTING.md ADDED
File without changes
pyproject.toml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aegis-agent"
7
+ version = "0.1.0"
8
+ description = "A modular, reliable AI agent engineering framework."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "Giancarlo Vizhnay" }
14
+ ]
15
+ dependencies = [
16
+ "pydantic>=2.10,<3.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "mypy>=1.14",
22
+ "pytest>=8.3",
23
+ "pytest-cov>=6.0",
24
+ "pytest-asyncio>=0.25",
25
+ "ruff>=0.9",
26
+ ]
27
+
28
+ [project.scripts]
29
+ aegis = "aegis.cli.app:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/aegis"]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
36
+ pythonpath = ["src"]
37
+ addopts = "-q --strict-markers --disable-warnings"
38
+
39
+ [tool.ruff]
40
+ target-version = "py311"
41
+ line-length = 100
42
+ src = ["src", "tests"]
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E", "F", "I", "B", "UP", "SIM", "RUF"]
46
+
47
+ [tool.mypy]
48
+ python_version = "3.11"
49
+ strict = true
50
+ mypy_path = "src"
51
+ packages = ["aegis"]
src/aegis/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aegis.core import Agent, AgentContext, AgentResult, Task, TaskStatus
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = [
6
+ "Agent",
7
+ "AgentContext",
8
+ "AgentResult",
9
+ "Task",
10
+ "TaskStatus",
11
+ ]
src/aegis/agents/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from aegis.agents.evaluator import EvaluationReport, EvaluatorAgent
2
+ from aegis.agents.planner import ExecutionPlan, PlannerAgent, PlanStep
3
+
4
+ __all__ = [
5
+ "EvaluationReport",
6
+ "EvaluatorAgent",
7
+ "ExecutionPlan",
8
+ "PlanStep",
9
+ "PlannerAgent",
10
+ ]
src/aegis/agents/evaluator.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from aegis.core import Agent, AgentContext, AgentResult
8
+
9
+
10
+ class EvaluationReport(BaseModel):
11
+ model_config = ConfigDict(extra="forbid", frozen=True)
12
+
13
+ passed: bool
14
+ score: int = Field(ge=0, le=100)
15
+ issues: tuple[str, ...] = ()
16
+ evaluated_agent: str = Field(min_length=1)
17
+
18
+
19
+ class EvaluatorAgent(Agent):
20
+ name = "evaluator"
21
+
22
+ async def run(self, context: AgentContext) -> AgentResult:
23
+ raw_result: Any = context.inputs.get("result")
24
+
25
+ if raw_result is None:
26
+ return AgentResult(
27
+ agent_name=self.name,
28
+ success=False,
29
+ errors=["Missing required input: result"],
30
+ )
31
+
32
+ try:
33
+ result = AgentResult.model_validate(raw_result)
34
+ except ValueError as exc:
35
+ return AgentResult(
36
+ agent_name=self.name,
37
+ success=False,
38
+ errors=[f"Invalid agent result: {exc}"],
39
+ )
40
+
41
+ issues: list[str] = []
42
+
43
+ if not result.success:
44
+ issues.append("Agent execution reported failure.")
45
+
46
+ if not result.output:
47
+ issues.append("Agent result contains no output.")
48
+
49
+ if result.errors:
50
+ issues.append("Agent result contains errors.")
51
+
52
+ score = max(0, 100 - (25 * len(issues)))
53
+ passed = not issues
54
+
55
+ report = EvaluationReport(
56
+ passed=passed,
57
+ score=score,
58
+ issues=tuple(issues),
59
+ evaluated_agent=result.agent_name,
60
+ )
61
+
62
+ return AgentResult(
63
+ agent_name=self.name,
64
+ success=True,
65
+ output=report.model_dump(),
66
+ )
src/aegis/agents/planner.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from aegis.core import Agent, AgentContext, AgentResult
6
+
7
+
8
+ class PlanStep(BaseModel):
9
+ id: int = Field(ge=1)
10
+ action: str = Field(min_length=1)
11
+ expected_output: str = Field(min_length=1)
12
+
13
+
14
+ class ExecutionPlan(BaseModel):
15
+ task_name: str = Field(min_length=1)
16
+ steps: tuple[PlanStep, ...] = Field(min_length=1)
17
+
18
+
19
+ class PlannerAgent(Agent):
20
+ name = "planner"
21
+
22
+ async def run(self, context: AgentContext) -> AgentResult:
23
+ task = context.task
24
+
25
+ plan = ExecutionPlan(
26
+ task_name=task.name,
27
+ steps=(
28
+ PlanStep(
29
+ id=1,
30
+ action="Analyze the task requirements.",
31
+ expected_output="A clear statement of scope and constraints.",
32
+ ),
33
+ PlanStep(
34
+ id=2,
35
+ action="Select the required tools and agents.",
36
+ expected_output="A bounded execution strategy.",
37
+ ),
38
+ PlanStep(
39
+ id=3,
40
+ action="Execute and validate the work.",
41
+ expected_output="A verified result or explicit failure state.",
42
+ ),
43
+ ),
44
+ )
45
+
46
+ return AgentResult(
47
+ agent_name=self.name,
48
+ success=True,
49
+ output=plan.model_dump(),
50
+ )
src/aegis/cli/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from aegis.cli.app import main
2
+
3
+ __all__ = ["main"]
src/aegis/cli/app.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from collections.abc import Sequence
5
+
6
+ from aegis.cli.commands.run import add_arguments, run_command
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(prog="aegis", description="Aegis CLI")
11
+ subparsers = parser.add_subparsers(dest="command")
12
+
13
+ run_parser = subparsers.add_parser("run", help="Run a task through Aegis")
14
+ add_arguments(run_parser)
15
+ run_parser.set_defaults(command_handler=run_command)
16
+
17
+ return parser
18
+
19
+
20
+ def main(argv: Sequence[str] | None = None) -> int:
21
+ parser = build_parser()
22
+
23
+ try:
24
+ args = parser.parse_args(list(argv) if argv is not None else None)
25
+ except SystemExit as exc:
26
+ return int(exc.code) if isinstance(exc.code, int) else 1
27
+
28
+ command_handler = getattr(args, "command_handler", None)
29
+ if command_handler is None:
30
+ parser.print_help()
31
+ return 0
32
+
33
+ return int(command_handler(args))
34
+
35
+
36
+ if __name__ == "__main__":
37
+ raise SystemExit(main())
src/aegis/cli/commands/__init__.py ADDED
File without changes
src/aegis/cli/commands/run.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ import traceback
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from aegis.cli.output import format_run_json, format_run_text
10
+ from aegis.runtime import (
11
+ AegisRunError,
12
+ AegisRuntime,
13
+ EvaluationFailureError,
14
+ ExecutionFailureError,
15
+ InternalFailureError,
16
+ InvalidConfigurationError,
17
+ InvalidTaskError,
18
+ PolicyViolationError,
19
+ RunOutcome,
20
+ UnknownAgentError,
21
+ UnknownWorkflowError,
22
+ )
23
+
24
+
25
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
26
+ parser.add_argument(
27
+ "task",
28
+ help="Task to run through Aegis.",
29
+ )
30
+ group = parser.add_mutually_exclusive_group()
31
+ group.add_argument(
32
+ "--agent",
33
+ help="Run a specific registered agent.",
34
+ )
35
+ group.add_argument(
36
+ "--workflow",
37
+ help="Run a named workflow.",
38
+ )
39
+ parser.add_argument(
40
+ "--output",
41
+ choices=("text", "json"),
42
+ default="text",
43
+ help="Output format.",
44
+ )
45
+ parser.add_argument(
46
+ "--max-steps",
47
+ type=_positive_int,
48
+ default=None,
49
+ help="Maximum execution steps for the run.",
50
+ )
51
+ parser.add_argument(
52
+ "--verbose",
53
+ action="store_true",
54
+ help="Show debug details for unexpected failures.",
55
+ )
56
+
57
+
58
+ def run_command(args: argparse.Namespace, runtime: Any | None = None) -> int:
59
+ runner: Any = runtime or AegisRuntime()
60
+ run_uuid = uuid4()
61
+ run_id = f"run_{run_uuid.hex}"
62
+ requested_agent = _normalize_optional(str(args.agent)) if getattr(args, "agent", None) else None
63
+ requested_workflow = (
64
+ _normalize_optional(str(args.workflow)) if getattr(args, "workflow", None) else None
65
+ )
66
+ max_steps = int(args.max_steps) if getattr(args, "max_steps", None) is not None else None
67
+
68
+ try:
69
+ outcome = runner.run(
70
+ task=str(args.task),
71
+ agent_name=requested_agent,
72
+ workflow_name=requested_workflow,
73
+ max_steps=max_steps,
74
+ run_id=run_uuid,
75
+ )
76
+ except (
77
+ InvalidTaskError,
78
+ InvalidConfigurationError,
79
+ UnknownAgentError,
80
+ UnknownWorkflowError,
81
+ ) as exc:
82
+ return _emit_failure(
83
+ run_id=run_id,
84
+ status="failed",
85
+ agent=requested_agent,
86
+ workflow=requested_workflow,
87
+ message=str(exc),
88
+ exit_code=exc.exit_code,
89
+ output_format=args.output,
90
+ verbose=args.verbose,
91
+ )
92
+ except PolicyViolationError as exc:
93
+ return _emit_failure(
94
+ run_id=run_id,
95
+ status="failed",
96
+ agent=requested_agent,
97
+ workflow=requested_workflow,
98
+ message=str(exc),
99
+ exit_code=exc.exit_code,
100
+ output_format=args.output,
101
+ verbose=args.verbose,
102
+ )
103
+ except (ExecutionFailureError, EvaluationFailureError) as exc:
104
+ return _emit_failure(
105
+ run_id=run_id,
106
+ status="failed",
107
+ agent=requested_agent,
108
+ workflow=requested_workflow,
109
+ message=str(exc),
110
+ exit_code=exc.exit_code,
111
+ output_format=args.output,
112
+ verbose=args.verbose,
113
+ )
114
+ except AegisRunError as exc:
115
+ return _emit_failure(
116
+ run_id=run_id,
117
+ status="failed",
118
+ agent=requested_agent,
119
+ workflow=requested_workflow,
120
+ message=str(exc),
121
+ exit_code=exc.exit_code,
122
+ output_format=args.output,
123
+ verbose=args.verbose,
124
+ )
125
+ except Exception as exc: # pragma: no cover - defensive boundary
126
+ error = InternalFailureError("Unexpected internal error.")
127
+ return _emit_failure(
128
+ run_id=run_id,
129
+ status="failed",
130
+ agent=requested_agent,
131
+ workflow=requested_workflow,
132
+ message=str(error),
133
+ exit_code=error.exit_code,
134
+ output_format=args.output,
135
+ verbose=args.verbose,
136
+ exc=exc,
137
+ )
138
+
139
+ if args.output == "json":
140
+ print(format_run_json(outcome))
141
+ else:
142
+ print(format_run_text(outcome))
143
+
144
+ return 0
145
+
146
+
147
+
148
+
149
+ def _emit_failure(
150
+ *,
151
+ run_id: str,
152
+ status: str,
153
+ agent: str | None,
154
+ workflow: str | None,
155
+ message: str,
156
+ exit_code: int,
157
+ output_format: str,
158
+ verbose: bool,
159
+ exc: BaseException | None = None,
160
+ ) -> int:
161
+ outcome = RunOutcome(
162
+ run_id=run_id,
163
+ status=status,
164
+ agent=agent,
165
+ workflow=workflow,
166
+ result=None,
167
+ evaluation=None,
168
+ error=message,
169
+ )
170
+
171
+ if output_format == "json":
172
+ print(format_run_json(outcome))
173
+ else:
174
+ _emit_error(message, verbose, exc)
175
+
176
+ return exit_code
177
+
178
+
179
+ def _emit_error(message: str, verbose: bool, exc: BaseException | None = None) -> None:
180
+ print(f"Error: {message}", file=sys.stderr)
181
+ if verbose and exc is not None:
182
+ traceback.print_exception(exc, file=sys.stderr)
183
+
184
+
185
+ def _normalize_optional(value: str) -> str:
186
+ return value.strip()
187
+
188
+
189
+ def _positive_int(value: str) -> int:
190
+ parsed = int(value)
191
+ if parsed < 1:
192
+ raise argparse.ArgumentTypeError("must be a positive integer")
193
+ return parsed
src/aegis/cli/output.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Mapping
5
+ from typing import Any
6
+
7
+ from aegis.runtime.runner import RunOutcome
8
+
9
+
10
+ def format_run_text(outcome: RunOutcome) -> str:
11
+ lines: list[str] = [
12
+ f"Run ID: {outcome.run_id}",
13
+ f"Status: {outcome.status}",
14
+ ]
15
+
16
+ if outcome.agent is not None:
17
+ lines.append(f"Agent: {outcome.agent}")
18
+
19
+ if outcome.workflow is not None:
20
+ lines.append(f"Workflow: {outcome.workflow}")
21
+
22
+ if outcome.result is not None:
23
+ lines.append("Result:")
24
+ lines.append(_format_payload(outcome.result))
25
+ else:
26
+ lines.append("Result:")
27
+ lines.append("<none>")
28
+
29
+ if outcome.evaluation is not None:
30
+ lines.append("Evaluation:")
31
+ lines.append(_format_payload(outcome.evaluation))
32
+
33
+ if outcome.error is not None:
34
+ lines.append("Error:")
35
+ lines.append(outcome.error)
36
+
37
+ return "\n".join(lines)
38
+
39
+
40
+ def format_run_json(outcome: RunOutcome) -> str:
41
+ return json.dumps(outcome.model_dump(mode="json"), indent=2, sort_keys=True)
42
+
43
+
44
+ def _format_payload(payload: Mapping[str, Any]) -> str:
45
+ return json.dumps(payload, indent=2, sort_keys=True)
src/aegis/core/__init__.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aegis.core.conductor import Conductor
2
+ from aegis.core.contracts import Agent, ModelProvider, Tool
3
+ from aegis.core.memory import (
4
+ DuplicateMemoryKeyError,
5
+ MemoryEntry,
6
+ MemoryKeyNotFoundError,
7
+ MemoryStore,
8
+ MemoryStoreError,
9
+ )
10
+ from aegis.core.models import AgentContext, AgentResult, Task, TaskStatus
11
+ from aegis.core.registry import (
12
+ AgentNotFoundError,
13
+ AgentRegistry,
14
+ AgentRegistryError,
15
+ DuplicateAgentError,
16
+ )
17
+ from aegis.core.tool_runner import ToolRunner
18
+ from aegis.core.workflow import WorkflowEngine, WorkflowResult
19
+
20
+ __all__ = [
21
+ "Agent",
22
+ "AgentContext",
23
+ "AgentNotFoundError",
24
+ "AgentRegistry",
25
+ "AgentRegistryError",
26
+ "AgentResult",
27
+ "Conductor",
28
+ "DuplicateAgentError",
29
+ "DuplicateMemoryKeyError",
30
+ "MemoryEntry",
31
+ "MemoryKeyNotFoundError",
32
+ "MemoryStore",
33
+ "MemoryStoreError",
34
+ "ModelProvider",
35
+ "Task",
36
+ "TaskStatus",
37
+ "Tool",
38
+ "ToolRunner",
39
+ "WorkflowEngine",
40
+ "WorkflowResult",
41
+ ]
src/aegis/core/conductor.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from aegis.core.models import AgentContext, AgentResult
4
+ from aegis.core.registry import AgentRegistry
5
+
6
+
7
+ class Conductor:
8
+ def __init__(self, registry: AgentRegistry) -> None:
9
+ self._registry = registry
10
+
11
+ async def execute(
12
+ self,
13
+ agent_name: str,
14
+ context: AgentContext,
15
+ ) -> AgentResult:
16
+ agent = self._registry.get(agent_name)
17
+ result = await agent.run(context)
18
+
19
+ if result.agent_name != agent.name:
20
+ raise ValueError("Agent result name does not match the registered agent name.")
21
+
22
+ return result
src/aegis/core/contracts.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from aegis.core.models import AgentContext, AgentResult
7
+
8
+
9
+ class Agent(ABC):
10
+ name: str
11
+
12
+ @abstractmethod
13
+ async def run(self, context: AgentContext) -> AgentResult:
14
+ """Execute the agent against the supplied context."""
15
+ raise NotImplementedError
16
+
17
+
18
+ @runtime_checkable
19
+ class ModelProvider(Protocol):
20
+ async def generate(self, prompt: str) -> str:
21
+ """Generate model output for a prompt."""
22
+ ...
23
+
24
+
25
+ @runtime_checkable
26
+ class Tool(Protocol):
27
+ name: str
28
+
29
+ async def execute(self, **kwargs: object) -> object:
30
+ """Execute the tool with validated arguments."""
31
+ ...
src/aegis/core/memory.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ class MemoryStoreError(Exception):
10
+ """Base error for memory store operations."""
11
+
12
+
13
+ class DuplicateMemoryKeyError(MemoryStoreError):
14
+ """Raised when adding a key that already exists."""
15
+
16
+
17
+ class MemoryKeyNotFoundError(MemoryStoreError):
18
+ """Raised when a requested key does not exist."""
19
+
20
+
21
+ class MemoryEntry(BaseModel):
22
+ model_config = ConfigDict(extra="forbid", frozen=True)
23
+
24
+ key: str = Field(min_length=1)
25
+ value: Any
26
+ metadata: dict[str, Any] = Field(default_factory=dict)
27
+
28
+
29
+ class MemoryStore:
30
+ def __init__(self, entries: Iterable[MemoryEntry] = ()) -> None:
31
+ self._entries: dict[str, MemoryEntry] = {}
32
+
33
+ for entry in entries:
34
+ self.add(entry)
35
+
36
+ def add(self, entry: MemoryEntry) -> None:
37
+ key = self._normalize_key(entry.key)
38
+
39
+ if key in self._entries:
40
+ raise DuplicateMemoryKeyError(f"Memory key already exists: {key}")
41
+
42
+ normalized_entry = entry.model_copy(update={"key": key})
43
+ self._entries[key] = normalized_entry
44
+
45
+ def set(self, entry: MemoryEntry) -> None:
46
+ key = self._normalize_key(entry.key)
47
+ normalized_entry = entry.model_copy(update={"key": key})
48
+ self._entries[key] = normalized_entry
49
+
50
+ def get(self, key: str) -> MemoryEntry:
51
+ normalized_key = self._normalize_key(key)
52
+
53
+ try:
54
+ return self._entries[normalized_key]
55
+ except KeyError as exc:
56
+ raise MemoryKeyNotFoundError(f"Memory key not found: {normalized_key}") from exc
57
+
58
+ def delete(self, key: str) -> None:
59
+ normalized_key = self._normalize_key(key)
60
+
61
+ if normalized_key not in self._entries:
62
+ raise MemoryKeyNotFoundError(f"Memory key not found: {normalized_key}")
63
+
64
+ del self._entries[normalized_key]
65
+
66
+ def contains(self, key: str) -> bool:
67
+ return self._normalize_key(key) in self._entries
68
+
69
+ def keys(self) -> tuple[str, ...]:
70
+ return tuple(sorted(self._entries))
71
+
72
+ def clear(self) -> None:
73
+ self._entries.clear()
74
+
75
+ def __len__(self) -> int:
76
+ return len(self._entries)
77
+
78
+ @staticmethod
79
+ def _normalize_key(key: str) -> str:
80
+ normalized = key.strip()
81
+
82
+ if not normalized:
83
+ raise ValueError("Memory key must not be empty.")
84
+
85
+ return normalized
src/aegis/core/models.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+ from uuid import UUID, uuid4
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class TaskStatus(StrEnum):
11
+ PENDING = "pending"
12
+ RUNNING = "running"
13
+ COMPLETED = "completed"
14
+ FAILED = "failed"
15
+ BLOCKED = "blocked"
16
+
17
+
18
+ class Task(BaseModel):
19
+ model_config = ConfigDict(extra="forbid", frozen=True)
20
+
21
+ id: UUID = Field(default_factory=uuid4)
22
+ name: str = Field(min_length=1, max_length=120)
23
+ description: str = Field(min_length=1)
24
+ status: TaskStatus = TaskStatus.PENDING
25
+ metadata: dict[str, Any] = Field(default_factory=dict)
26
+
27
+
28
+ class AgentContext(BaseModel):
29
+ model_config = ConfigDict(extra="forbid")
30
+
31
+ run_id: UUID = Field(default_factory=uuid4)
32
+ task: Task
33
+ inputs: dict[str, Any] = Field(default_factory=dict)
34
+ state: dict[str, Any] = Field(default_factory=dict)
35
+
36
+
37
+ class AgentResult(BaseModel):
38
+ model_config = ConfigDict(extra="forbid")
39
+
40
+ agent_name: str = Field(min_length=1)
41
+ success: bool
42
+ output: dict[str, Any] = Field(default_factory=dict)
43
+ errors: list[str] = Field(default_factory=list)
44
+ metadata: dict[str, Any] = Field(default_factory=dict)
src/aegis/core/policies.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class ApprovalMode(StrEnum):
9
+ NEVER = "never"
10
+ HIGH_RISK_ONLY = "high_risk_only"
11
+ ALWAYS = "always"
12
+
13
+
14
+ class ResponsePolicy(BaseModel):
15
+ model_config = ConfigDict(extra="forbid", frozen=True)
16
+
17
+ max_input_chars: int = Field(default=20_000, ge=1)
18
+ max_output_tokens: int = Field(default=600, ge=1)
19
+
20
+ answer_first: bool = True
21
+ include_reasoning: bool = False
22
+ include_sources: bool = True
23
+ include_warnings: bool = True
24
+ allow_repetition: bool = False
25
+
26
+
27
+ class ExecutionPolicy(BaseModel):
28
+ model_config = ConfigDict(extra="forbid", frozen=True)
29
+
30
+ max_steps: int = Field(default=8, ge=1)
31
+ timeout_seconds: float = Field(default=60.0, gt=0)
32
+ retry_limit: int = Field(default=1, ge=0)
33
+ max_tool_calls: int = Field(default=6, ge=0)
34
+ max_parallel_tasks: int = Field(default=3, ge=1)
35
+
36
+ approval_mode: ApprovalMode = ApprovalMode.HIGH_RISK_ONLY
37
+ allow_network: bool = False
38
+ allow_file_writes: bool = False
39
+ allow_shell: bool = False
40
+
41
+
42
+ class AgentPolicy(BaseModel):
43
+ model_config = ConfigDict(extra="forbid", frozen=True)
44
+
45
+ response: ResponsePolicy = Field(default_factory=ResponsePolicy)
46
+ execution: ExecutionPolicy = Field(default_factory=ExecutionPolicy)
src/aegis/core/registry.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from aegis.core.contracts import Agent
6
+
7
+
8
+ class AgentRegistryError(Exception):
9
+ """Base registry error."""
10
+
11
+
12
+ class DuplicateAgentError(AgentRegistryError):
13
+ """Raised when an agent name is already registered."""
14
+
15
+
16
+ class AgentNotFoundError(AgentRegistryError):
17
+ """Raised when an agent cannot be found."""
18
+
19
+
20
+ class AgentRegistry:
21
+ def __init__(self, agents: Iterable[Agent] | None = None) -> None:
22
+ self._agents: dict[str, Agent] = {}
23
+
24
+ if agents is not None:
25
+ for agent in agents:
26
+ self.register(agent)
27
+
28
+ def register(self, agent: Agent) -> None:
29
+ name = agent.name.strip()
30
+
31
+ if not name:
32
+ raise ValueError("Agent name cannot be empty.")
33
+
34
+ if name in self._agents:
35
+ raise DuplicateAgentError(f"Agent already registered: {name}")
36
+
37
+ self._agents[name] = agent
38
+
39
+ def get(self, name: str) -> Agent:
40
+ normalized_name = name.strip()
41
+
42
+ try:
43
+ return self._agents[normalized_name]
44
+ except KeyError as exc:
45
+ raise AgentNotFoundError(f"Agent not found: {normalized_name}") from exc
46
+
47
+ def contains(self, name: str) -> bool:
48
+ return name.strip() in self._agents
49
+
50
+ def names(self) -> tuple[str, ...]:
51
+ return tuple(sorted(self._agents))
52
+
53
+ def __len__(self) -> int:
54
+ return len(self._agents)
src/aegis/core/tool_registry.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from aegis.core.contracts import Tool
6
+
7
+
8
+ class ToolRegistryError(Exception):
9
+ """Base tool registry error."""
10
+
11
+
12
+ class DuplicateToolError(ToolRegistryError):
13
+ """Raised when a tool name is already registered."""
14
+
15
+
16
+ class ToolNotFoundError(ToolRegistryError):
17
+ """Raised when a tool cannot be found."""
18
+
19
+
20
+ class ToolRegistry:
21
+ def __init__(self, tools: Iterable[Tool] | None = None) -> None:
22
+ self._tools: dict[str, Tool] = {}
23
+
24
+ if tools is not None:
25
+ for tool in tools:
26
+ self.register(tool)
27
+
28
+ def register(self, tool: Tool) -> None:
29
+ name = tool.name.strip()
30
+
31
+ if not name:
32
+ raise ValueError("Tool name cannot be empty.")
33
+
34
+ if name in self._tools:
35
+ raise DuplicateToolError(f"Tool already registered: {name}")
36
+
37
+ self._tools[name] = tool
38
+
39
+ def get(self, name: str) -> Tool:
40
+ normalized_name = name.strip()
41
+
42
+ try:
43
+ return self._tools[normalized_name]
44
+ except KeyError as exc:
45
+ raise ToolNotFoundError(f"Tool not found: {normalized_name}") from exc
46
+
47
+ def contains(self, name: str) -> bool:
48
+ return name.strip() in self._tools
49
+
50
+ def names(self) -> tuple[str, ...]:
51
+ return tuple(sorted(self._tools))
52
+
53
+ def __len__(self) -> int:
54
+ return len(self._tools)
src/aegis/core/tool_runner.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from aegis.core.tool_registry import ToolRegistry
4
+
5
+
6
+ class ToolRunner:
7
+ def __init__(self, registry: ToolRegistry) -> None:
8
+ self._registry = registry
9
+
10
+ async def execute(
11
+ self,
12
+ tool_name: str,
13
+ **kwargs: object,
14
+ ) -> object:
15
+ tool = self._registry.get(tool_name)
16
+ return await tool.execute(**kwargs)
src/aegis/core/workflow.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+ from aegis.core.conductor import Conductor
6
+ from aegis.core.memory import MemoryEntry, MemoryStore
7
+ from aegis.core.models import AgentContext, AgentResult
8
+
9
+
10
+ class WorkflowResult(BaseModel):
11
+ model_config = ConfigDict(extra="forbid", frozen=True)
12
+
13
+ plan: AgentResult
14
+ execution: AgentResult
15
+ evaluation: AgentResult
16
+ memory_keys: tuple[str, ...] = Field(default_factory=tuple)
17
+
18
+
19
+ class WorkflowEngine:
20
+ def __init__(
21
+ self,
22
+ conductor: Conductor,
23
+ memory: MemoryStore,
24
+ ) -> None:
25
+ self._conductor = conductor
26
+ self._memory = memory
27
+
28
+ async def run(
29
+ self,
30
+ context: AgentContext,
31
+ target_agent: str,
32
+ ) -> WorkflowResult:
33
+ plan_result = await self._conductor.execute("planner", context)
34
+ self._store_result(context, "plan", plan_result)
35
+
36
+ if not plan_result.success:
37
+ raise RuntimeError("Planner failed to produce a valid plan.")
38
+
39
+ execution_result = await self._conductor.execute(
40
+ target_agent,
41
+ context,
42
+ )
43
+ self._store_result(context, "execution", execution_result)
44
+
45
+ evaluation_context = context.model_copy(
46
+ deep=True,
47
+ update={
48
+ "inputs": {
49
+ **context.inputs,
50
+ "result": execution_result.model_dump(),
51
+ }
52
+ },
53
+ )
54
+
55
+ evaluation_result = await self._conductor.execute(
56
+ "evaluator",
57
+ evaluation_context,
58
+ )
59
+ self._store_result(context, "evaluation", evaluation_result)
60
+
61
+ return WorkflowResult(
62
+ plan=plan_result,
63
+ execution=execution_result,
64
+ evaluation=evaluation_result,
65
+ memory_keys=self._workflow_memory_keys(context),
66
+ )
67
+
68
+ def _store_result(
69
+ self,
70
+ context: AgentContext,
71
+ stage: str,
72
+ result: AgentResult,
73
+ ) -> None:
74
+ key = self._memory_key(context, stage)
75
+
76
+ self._memory.set(
77
+ MemoryEntry(
78
+ key=key,
79
+ value=result.model_dump(),
80
+ metadata={
81
+ "run_id": str(context.run_id),
82
+ "stage": stage,
83
+ "agent_name": result.agent_name,
84
+ },
85
+ )
86
+ )
87
+
88
+ def _workflow_memory_keys(
89
+ self,
90
+ context: AgentContext,
91
+ ) -> tuple[str, ...]:
92
+ prefix = f"workflow.{context.run_id}."
93
+
94
+ return tuple(key for key in self._memory.keys() if key.startswith(prefix)) # noqa: SIM118
95
+
96
+ @staticmethod
97
+ def _memory_key(
98
+ context: AgentContext,
99
+ stage: str,
100
+ ) -> str:
101
+ return f"workflow.{context.run_id}.{stage}"
src/aegis/evaluation/__init__.py ADDED
File without changes
src/aegis/memory/__init__.py ADDED
File without changes
src/aegis/observability/__init__.py ADDED
File without changes
src/aegis/providers/__init__.py ADDED
File without changes
src/aegis/runtime/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aegis.runtime.errors import (
2
+ AegisRunError,
3
+ EvaluationFailureError,
4
+ ExecutionFailureError,
5
+ InternalFailureError,
6
+ InvalidConfigurationError,
7
+ InvalidTaskError,
8
+ PolicyViolationError,
9
+ UnknownAgentError,
10
+ UnknownWorkflowError,
11
+ )
12
+ from aegis.runtime.runner import AegisRuntime, RunOutcome, WorkflowDefinition
13
+
14
+ __all__ = [
15
+ "AegisRunError",
16
+ "AegisRuntime",
17
+ "EvaluationFailureError",
18
+ "ExecutionFailureError",
19
+ "InternalFailureError",
20
+ "InvalidConfigurationError",
21
+ "InvalidTaskError",
22
+ "PolicyViolationError",
23
+ "RunOutcome",
24
+ "UnknownAgentError",
25
+ "UnknownWorkflowError",
26
+ "WorkflowDefinition",
27
+ ]
src/aegis/runtime/errors.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ class AegisRunError(Exception):
5
+ exit_code = 1
6
+
7
+
8
+ class InvalidTaskError(AegisRunError):
9
+ exit_code = 2
10
+
11
+
12
+ class InvalidConfigurationError(AegisRunError):
13
+ exit_code = 3
14
+
15
+
16
+ class UnknownAgentError(InvalidConfigurationError):
17
+ pass
18
+
19
+
20
+ class UnknownWorkflowError(InvalidConfigurationError):
21
+ pass
22
+
23
+
24
+ class PolicyViolationError(AegisRunError):
25
+ exit_code = 4
26
+
27
+
28
+ class ExecutionFailureError(AegisRunError):
29
+ exit_code = 5
30
+
31
+
32
+ class EvaluationFailureError(AegisRunError):
33
+ exit_code = 6
34
+
35
+
36
+ class InternalFailureError(AegisRunError):
37
+ exit_code = 1