diff --git a/README.md b/README.md index bb97bc6..6f42fc1 100755 --- a/README.md +++ b/README.md @@ -162,9 +162,10 @@ execution result and contract results. An agent can use this feedback to revise the draft task and retry until the contracts pass or the loop reaches its attempt limit. -The lowering integration example is in `examples/agent`. Its draft currently -contains an intentionally disabled OP-IR update so the contract-failure path -can be exercised; uncomment those lines to restore the successful task. +The lowering integration example lives in the Graphite repository, under +`examples/agent`. It starts from a draft task whose OP-IR update is disabled, +so the first attempt fails a contract, and runs this loop until the revised +draft passes. ### Pydantic AI task generation diff --git a/loom/agent/agent.py b/loom/agent/agent.py index 64d4df4..bd76783 100755 --- a/loom/agent/agent.py +++ b/loom/agent/agent.py @@ -8,6 +8,7 @@ from __future__ import annotations from dataclasses import dataclass +from pathlib import Path from typing import Any, Callable, Mapping, Protocol from ..core import Pipeline, PipelineResult, Task diff --git a/loom/agent/task_gen.py b/loom/agent/task_gen.py index 55c27b8..0d4d235 100755 --- a/loom/agent/task_gen.py +++ b/loom/agent/task_gen.py @@ -3,14 +3,13 @@ from __future__ import annotations import importlib.util +from collections.abc import Mapping from pathlib import Path -from typing import Iterable +from typing import Any, Iterable from uuid import uuid4 from pydantic import BaseModel, Field -from graphite import ContractException - from ..core import Pipeline, Task from .agent import AgentContext from .runner import TaskFactories, TaskFactory @@ -68,12 +67,24 @@ def _selected_paths(self, context: AgentContext) -> dict[str, Path]: return {name: context.draft[name] for name in self.tasks} @staticmethod - def _prompt(name: str, source: str, failure: Exception | None) -> str: + def _contract_failures(failure: Exception | None) -> Mapping[str, Any] | None: + """Return per-contract failures when ``failure`` carries them. + + Contract types live outside Loom, so the shape is duck-typed rather + than imported: ``loom.agent`` stays usable without a contract package + installed. + """ + failures = getattr(failure, "failures", None) + return failures if isinstance(failures, Mapping) and failures else None + + @classmethod + def _prompt(cls, name: str, source: str, failure: Exception | None) -> str: feedback = "No execution feedback is available yet." - if isinstance(failure, ContractException): + contract_failures = cls._contract_failures(failure) + if contract_failures is not None: feedback = "Contract failures:\n" + "\n".join( - f"- {contract}: {result.failures}" - for contract, result in failure.failures.items() + f"- {contract}: {getattr(result, 'failures', result)}" + for contract, result in contract_failures.items() ) elif failure is not None: feedback = f"Task/runtime exception: {type(failure).__name__}: {failure}"