From 04f76d9b7717a0ab87d9454937301775dd87d8b6 Mon Sep 17 00:00:00 2001 From: Arunima1405 Date: Tue, 1 Sep 2026 17:44:40 +0100 Subject: [PATCH] Make loom.agent importable without a contract package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `loom.agent.task_gen` imported `graphite.ContractException` at module import time, so `from loom.agent import AgentLoop` raised ImportError on a plain `pip install loom-workflow` — Loom declares no graphite dependency, and contracts live outside this package by design. The contract-failure shape is now duck-typed, which keeps the same feedback text without the import. Also import `Path` in `loom.agent.agent`, where it was used in an annotation but never imported (`get_type_hints(AgentContext.draft.fget)` raised NameError), and point the README at the agent example's current home in the Graphite repository. --- README.md | 7 ++++--- loom/agent/agent.py | 1 + loom/agent/task_gen.py | 25 ++++++++++++++++++------- 3 files changed, 23 insertions(+), 10 deletions(-) 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}"