Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: ci

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e '.[dev]'
- name: Lint
run: ruff check .
- name: Typecheck
run: mypy agent_core
- name: Test
run: pytest -q
- name: Schema export is up to date
run: |
python scripts/export_schemas.py
git diff --exit-code -- agent_core/contracts/schemas || (echo "schemas out of date: run scripts/export_schemas.py" && exit 1)
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__/
*.py[cod]
.venv/
venv/
.mypy_cache/
.ruff_cache/
.pytest_cache/
dist/
build/
*.egg-info/
.coverage
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# agent-core

Shared, dependency-light **typed contracts** for the AS215932 Agent Runtime Framework.

This is the **§31 safe milestone** (Phase 1) of the framework consolidation described in
`../docs/migration/first-safe-milestone.md`. It introduces standard contracts **without
changing any existing loop's behavior**:

- `agent_core/contracts/` — pydantic v2 models (JSON-serializable, schema-versioned).
Importing them pulls in **only pydantic** (no langgraph / pydantic-ai / db).
- `agent_core/adapters/` — pure mapping functions that convert each loop's existing
shapes (engineering-loop, NOC agent, knowledge) into the shared contracts. **Imported
by tests only**; not wired into any loop's runtime.
- `agent_core/contracts/graphs/` — *descriptive* draft `GraphSpec`s of the loops' current
LangGraph topology (no compiler yet).

## Scope

In: contracts, adapters (test-only), draft GraphSpecs, tests, CI.
Out (later phases): runtime, GraphSpec compiler, model router, tool/MCP registries,
memory store, learning substrate, judges, policy gates, control-plane API/GUI.

## Develop

```bash
uv venv && uv pip install -e '.[dev]' # or: python -m venv .venv && pip install -e '.[dev]'
ruff check . && mypy agent_core && pytest -q
python scripts/export_schemas.py # regenerate committed JSON schemas
```

See `../docs/` for the full inventory, contract-gap analysis, and migration plan.
5 changes: 5 additions & 0 deletions agent_core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""agent-core: shared typed contracts for the AS215932 Agent Runtime Framework."""

from __future__ import annotations

__version__ = "0.1.0"
12 changes: 12 additions & 0 deletions agent_core/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Adapters: pure mapping functions from each loop's shapes to agent-core contracts.

These are imported by tests only — they are NOT wired into any loop's runtime in this
milestone. Each function takes a JSON-serializable dict (the loop's existing shape) and
returns a typed contract.
"""

from __future__ import annotations

from agent_core.adapters import engineering_loop, knowledge, noc_agent

__all__ = ["engineering_loop", "knowledge", "noc_agent"]
129 changes: 129 additions & 0 deletions agent_core/adapters/engineering_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Map engineering-loop shapes -> agent-core contracts (no runtime wiring).

Source shapes: hyrule_engineering_loop.state.GraphState, llm.RoleReviewOutput /
FileMutation, backend CostReport (in backend_results[].cost), trace.loop_trace.json.
"""

from __future__ import annotations

from typing import Any

from agent_core.contracts.decision import DecisionPacket
from agent_core.contracts.models import CostUsage
from agent_core.contracts.task import TaskEnvelope
from agent_core.contracts.tools import ToolResult
from agent_core.contracts.tracing import TraceEvent

GRAPH_ID = "engineering-loop"


def task_envelope_from_graph_state(state: dict[str, Any]) -> TaskEnvelope:
return TaskEnvelope.model_validate(
{
"task_id": state["change_id"],
"task_class": state.get("change_class", "mixed"),
"source": GRAPH_ID,
"risk_level": state.get("risk_level", "low"),
"customer_impact": state.get("customer_impact"),
"input": {
"source_of_truth_files": state.get("source_of_truth_files", []),
"feature_request": state.get("feature_request"),
},
"graph_id": GRAPH_ID,
}
)


def decision_from_role_review(role: str, review: dict[str, Any]) -> DecisionPacket:
approved = review.get("approved")
return DecisionPacket.model_validate(
{
"decision": "approve" if approved else "reject",
"approved": approved,
"rationale": review.get("notes", ""),
"proposed_actions": [
{"path": m.get("path"), "operation": m.get("operation", "create")}
for m in review.get("proposed_mutations", []) or []
],
"validation_errors": review.get("validation_errors", []) or [],
"agent_role": role,
"node_id": role,
"graph_id": GRAPH_ID,
}
)


def cost_usage_from_backend_result(backend_result: dict[str, Any]) -> CostUsage:
cost = backend_result.get("cost", {}) or {}
return CostUsage.model_validate(
{
"model": cost.get("model") or backend_result.get("model"),
"provider": cost.get("provider"),
"input_tokens": cost.get("input_tokens"),
"output_tokens": cost.get("output_tokens"),
"usd": cost.get("usd"),
}
)


def tool_result_from_gate(gate: dict[str, Any]) -> ToolResult:
return ToolResult.model_validate(
{
"tool": str(gate.get("command")),
"ok": gate.get("status") == "passed",
"output": {"status": gate.get("status"), "returncode": gate.get("returncode")},
"node_id": "gate_execution",
"graph_id": GRAPH_ID,
}
)


def trace_events_from_loop_trace(
trace: dict[str, Any], *, run_id: str | None = None
) -> list[TraceEvent]:
events: list[TraceEvent] = []
for item in trace.get("llm_outputs", []) or []:
events.append(
TraceEvent.model_validate(
{
"event_type": "model_call",
"node_id": item.get("role"),
"agent_role": item.get("role"),
"summary": f"role review approved={item.get('approved')}",
"payload": item,
"run_id": run_id,
"graph_id": GRAPH_ID,
}
)
)
for gate in trace.get("gate_results", []) or []:
events.append(
TraceEvent.model_validate(
{
"event_type": "tool_call",
"node_id": "gate_execution",
"summary": str(gate.get("command")),
"payload": gate,
"run_id": run_id,
"graph_id": GRAPH_ID,
}
)
)
for backend_result in trace.get("backend_results", []) or []:
events.append(
TraceEvent.model_validate(
{
"event_type": "backend_execution",
"node_id": "delegate_implementation",
"summary": (
f"backend={backend_result.get('backend')} "
f"status={backend_result.get('status')}"
),
"payload": backend_result,
"cost": cost_usage_from_backend_result(backend_result).model_dump(),
"run_id": run_id,
"graph_id": GRAPH_ID,
}
)
)
return events
90 changes: 90 additions & 0 deletions agent_core/adapters/knowledge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Map knowledge loop shapes -> agent-core contracts (no runtime wiring).

Source shapes: knowledge context-pack (schema/context-pack.schema.json) + CLI request.
"""

from __future__ import annotations

import hashlib
from typing import Any

from agent_core.contracts.evidence import EvidencePacket
from agent_core.contracts.task import TaskEnvelope
from agent_core.contracts.tracing import TraceEvent

GRAPH_ID = "knowledge"
_AUTHORITY_ORDER = ["A0", "A1", "A2", "A3", "A4", "A5"]


def task_envelope_from_context_request(
task: str,
role: str = "engineering_loop",
risk_level: str = "low",
task_id: str | None = None,
) -> TaskEnvelope:
resolved_id = task_id or "know_" + hashlib.sha256(task.encode()).hexdigest()[:8]
return TaskEnvelope.model_validate(
{
"task_id": resolved_id,
"task_class": "knowledge_retrieval",
"source": GRAPH_ID,
"risk_level": risk_level,
"input": {"task": task, "role": role},
"graph_id": GRAPH_ID,
}
)


def evidence_from_context_pack(pack: dict[str, Any]) -> EvidencePacket:
sources: list[dict[str, Any]] = []
best: str | None = None
for ref in pack.get("included_refs", []) or []:
authority = ref.get("authority")
sources.append(
{
"ref": ref.get("ref") or ref.get("doc_id") or ref.get("doc_path") or "",
"authority": authority,
"kind": ref.get("kind"),
"commit_sha": ref.get("commit_sha"),
"review_status": ref.get("review_status"),
}
)
if authority in _AUTHORITY_ORDER and (
best is None or _AUTHORITY_ORDER.index(authority) < _AUTHORITY_ORDER.index(best)
):
best = authority
return EvidencePacket.model_validate(
{
"sources": sources,
"authority_max": best,
"unresolved_questions": pack.get("unresolved_questions", []) or [],
"metadata": {
"context_pack_id": pack.get("id"),
"retrieval_version": pack.get("retrieval_version"),
"policy_version": pack.get("policy_version"),
"knowledge_snapshot": pack.get("knowledge_snapshot"),
},
"graph_id": GRAPH_ID,
}
)


def trace_event_from_context_pack(
pack: dict[str, Any], *, run_id: str | None = None
) -> TraceEvent:
ref_count = len(pack.get("included_refs", []) or [])
return TraceEvent.model_validate(
{
"event_type": "knowledge_context_pack",
"node_id": "context_pack",
"summary": f"context pack {pack.get('id')} ({ref_count} refs)",
"payload": {
"id": pack.get("id"),
"retrieval_version": pack.get("retrieval_version"),
"policy_version": pack.get("policy_version"),
"policy_decision": pack.get("policy_decision"),
},
"run_id": run_id,
"graph_id": GRAPH_ID,
}
)
Loading
Loading