Upload initial Aegis project
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +33 -0
- .mypy_cache/.gitignore +2 -0
- .mypy_cache/CACHEDIR.TAG +3 -0
- .pytest_cache/.gitignore +2 -0
- .pytest_cache/CACHEDIR.TAG +4 -0
- .pytest_cache/README.md +8 -0
- .pytest_cache/v/cache/nodeids +58 -0
- .ruff_cache/.gitignore +2 -0
- .ruff_cache/0.15.2/12944381863948167845 +0 -0
- .ruff_cache/0.15.2/9042146699542604200 +0 -0
- .ruff_cache/0.16.0/3506816079193222284 +0 -0
- .ruff_cache/0.16.0/6668019937987631587 +0 -0
- .ruff_cache/0.16.0/9602145004845211386 +0 -0
- .ruff_cache/CACHEDIR.TAG +1 -0
- AGENTS.md +0 -0
- API.md +0 -0
- ARCHITECTURE.md +0 -0
- CHANGELOG.md +0 -0
- CONTRIBUTING.md +0 -0
- LICENSE +0 -0
- README.md +101 -0
- ROADMAP.md +0 -0
- SECURITY.md +0 -0
- TESTING.md +0 -0
- pyproject.toml +51 -0
- src/aegis/__init__.py +11 -0
- src/aegis/agents/__init__.py +10 -0
- src/aegis/agents/evaluator.py +66 -0
- src/aegis/agents/planner.py +50 -0
- src/aegis/cli/__init__.py +3 -0
- src/aegis/cli/app.py +37 -0
- src/aegis/cli/commands/__init__.py +0 -0
- src/aegis/cli/commands/run.py +193 -0
- src/aegis/cli/output.py +45 -0
- src/aegis/core/__init__.py +41 -0
- src/aegis/core/conductor.py +22 -0
- src/aegis/core/contracts.py +31 -0
- src/aegis/core/memory.py +85 -0
- src/aegis/core/models.py +44 -0
- src/aegis/core/policies.py +46 -0
- src/aegis/core/registry.py +54 -0
- src/aegis/core/tool_registry.py +54 -0
- src/aegis/core/tool_runner.py +16 -0
- src/aegis/core/workflow.py +101 -0
- src/aegis/evaluation/__init__.py +0 -0
- src/aegis/memory/__init__.py +0 -0
- src/aegis/observability/__init__.py +0 -0
- src/aegis/providers/__init__.py +0 -0
- src/aegis/runtime/__init__.py +27 -0
- 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
|