contract4agents 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contract4agents/__init__.py +7 -0
- contract4agents/__main__.py +6 -0
- contract4agents/adapters/__init__.py +0 -0
- contract4agents/adapters/openai.py +169 -0
- contract4agents/ast.py +135 -0
- contract4agents/cli.py +143 -0
- contract4agents/compiler/__init__.py +310 -0
- contract4agents/diagnostics.py +39 -0
- contract4agents/docscheck.py +69 -0
- contract4agents/evaluation.py +84 -0
- contract4agents/expressions/__init__.py +31 -0
- contract4agents/expressions/_eval.py +152 -0
- contract4agents/expressions/_grammar.py +212 -0
- contract4agents/expressions/_model.py +29 -0
- contract4agents/expressions/_refs.py +18 -0
- contract4agents/expressions/_trace_ops.py +56 -0
- contract4agents/fixtures/__init__.py +123 -0
- contract4agents/fixtures/_artifacts.py +65 -0
- contract4agents/fixtures/_execution.py +83 -0
- contract4agents/fixtures/_models.py +75 -0
- contract4agents/fixtures/_reports.py +72 -0
- contract4agents/monitor.py +46 -0
- contract4agents/parser/__init__.py +57 -0
- contract4agents/parser/_grammar.py +91 -0
- contract4agents/parser/_transformer.py +323 -0
- contract4agents/parser/_values.py +61 -0
- contract4agents/runtime/__init__.py +53 -0
- contract4agents/runtime/_datasources.py +301 -0
- contract4agents/runtime/_errors.py +64 -0
- contract4agents/runtime/_tools.py +59 -0
- contract4agents/runtime/_trace.py +41 -0
- contract4agents/runtime/_trace_io.py +49 -0
- contract4agents/runtime/_utils.py +23 -0
- contract4agents/schema.py +86 -0
- contract4agents/semantics.py +335 -0
- contract4agents/visualization/__init__.py +30 -0
- contract4agents/visualization/_artifacts.py +23 -0
- contract4agents/visualization/_graph.py +185 -0
- contract4agents/visualization/_html.py +508 -0
- contract4agents/visualization/_mermaid.py +82 -0
- contract4agents/visualization/_types.py +60 -0
- contract4agents/visualization/_utils.py +39 -0
- contract4agents-0.1.0.dist-info/METADATA +132 -0
- contract4agents-0.1.0.dist-info/RECORD +47 -0
- contract4agents-0.1.0.dist-info/WHEEL +4 -0
- contract4agents-0.1.0.dist-info/entry_points.txt +5 -0
- contract4agents-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Contract4Agents Python package."""
|
|
2
|
+
|
|
3
|
+
from contract4agents.compiler import compile_project
|
|
4
|
+
from contract4agents.parser import parse_file, parse_project
|
|
5
|
+
from contract4agents.semantics import analyze_project
|
|
6
|
+
|
|
7
|
+
__all__ = ["analyze_project", "compile_project", "parse_file", "parse_project"]
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""OpenAI Agents SDK adapter.
|
|
2
|
+
|
|
3
|
+
The adapter is intentionally thin: Contract4Agents compiles to provider-neutral
|
|
4
|
+
manifests first, and this module projects those manifests onto OpenAI's SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from contract4agents.compiler import AgentManifest
|
|
14
|
+
from contract4agents.runtime import TraceRecorder
|
|
15
|
+
|
|
16
|
+
_RunHooksBase: type[Any]
|
|
17
|
+
try:
|
|
18
|
+
from agents import RunHooks as _ImportedRunHooks
|
|
19
|
+
|
|
20
|
+
_RunHooksBase = _ImportedRunHooks
|
|
21
|
+
except Exception: # noqa: BLE001 - optional adapter import boundary.
|
|
22
|
+
_RunHooksBase = object
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class OpenAIAdapterResult:
|
|
27
|
+
final_output: Any
|
|
28
|
+
last_agent: str | None
|
|
29
|
+
raw_result: Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OpenAIAdapterUnavailable(RuntimeError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def openai_tool_name(contract_name: str) -> str:
|
|
37
|
+
"""Convert a Contract4Agents capability name into an OpenAI-safe tool name."""
|
|
38
|
+
return contract_name.replace(".", "__")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def contract_tool_name(openai_name: str) -> str:
|
|
42
|
+
"""Convert a generated OpenAI tool name back into the Contract4Agents capability name."""
|
|
43
|
+
return openai_name.replace("__", ".")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OpenAITraceHooks(_RunHooksBase): # type: ignore[misc]
|
|
47
|
+
"""Minimal hook object that normalizes Agents SDK lifecycle events to Contract4Agents traces."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, trace: TraceRecorder) -> None:
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.trace = trace
|
|
52
|
+
|
|
53
|
+
async def on_agent_start(self, _context: Any, agent: Any) -> None:
|
|
54
|
+
self.trace.record("agent.started", agent=getattr(agent, "name", str(agent)))
|
|
55
|
+
|
|
56
|
+
async def on_agent_end(self, _context: Any, agent: Any, output: Any) -> None:
|
|
57
|
+
self.trace.record("agent.completed", agent=getattr(agent, "name", str(agent)), output=_serializable(output))
|
|
58
|
+
|
|
59
|
+
async def on_handoff(self, _context: Any, from_agent: Any, to_agent: Any) -> None:
|
|
60
|
+
self.trace.record(
|
|
61
|
+
"agent.handoff",
|
|
62
|
+
from_agent=getattr(from_agent, "name", str(from_agent)),
|
|
63
|
+
to_agent=getattr(to_agent, "name", str(to_agent)),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def on_tool_start(self, _context: Any, agent: Any, tool: Any) -> None:
|
|
67
|
+
self.trace.record(
|
|
68
|
+
"tool.started",
|
|
69
|
+
agent=getattr(agent, "name", str(agent)),
|
|
70
|
+
tool=contract_tool_name(getattr(tool, "name", str(tool))),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def on_tool_end(self, _context: Any, agent: Any, tool: Any, result: str) -> None:
|
|
74
|
+
self.trace.record(
|
|
75
|
+
"tool.completed",
|
|
76
|
+
agent=getattr(agent, "name", str(agent)),
|
|
77
|
+
tool=contract_tool_name(getattr(tool, "name", str(tool))),
|
|
78
|
+
result=_serializable(result),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def on_llm_start(self, _context: Any, agent: Any, _system_prompt: str | None, input_items: list[Any]) -> None:
|
|
82
|
+
self.trace.record("llm.started", agent=getattr(agent, "name", str(agent)), input_count=len(input_items))
|
|
83
|
+
|
|
84
|
+
async def on_llm_end(self, _context: Any, agent: Any, _response: Any) -> None:
|
|
85
|
+
self.trace.record("llm.completed", agent=getattr(agent, "name", str(agent)))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_openai_agent(
|
|
89
|
+
manifest: AgentManifest,
|
|
90
|
+
instructions: str,
|
|
91
|
+
tools: list[Any] | None = None,
|
|
92
|
+
handoffs: list[Any] | None = None,
|
|
93
|
+
output_type: Any | None = None,
|
|
94
|
+
hooks: Any | None = None,
|
|
95
|
+
input_guardrails: list[Any] | None = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
try:
|
|
98
|
+
from agents import Agent
|
|
99
|
+
except Exception as exc: # noqa: BLE001 - optional adapter import boundary.
|
|
100
|
+
raise OpenAIAdapterUnavailable("openai-agents is not installed") from exc
|
|
101
|
+
kwargs: dict[str, Any] = {
|
|
102
|
+
"name": manifest["agent"],
|
|
103
|
+
"instructions": instructions,
|
|
104
|
+
"model": manifest.get("model", "gpt-5.5"),
|
|
105
|
+
"tools": tools or [],
|
|
106
|
+
"handoffs": handoffs or [],
|
|
107
|
+
}
|
|
108
|
+
if output_type is not None:
|
|
109
|
+
kwargs["output_type"] = output_type
|
|
110
|
+
if hooks is not None:
|
|
111
|
+
kwargs["hooks"] = hooks
|
|
112
|
+
if input_guardrails is not None:
|
|
113
|
+
kwargs["input_guardrails"] = input_guardrails
|
|
114
|
+
return Agent(**kwargs)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def run_openai_agent(
|
|
118
|
+
agent: Any,
|
|
119
|
+
user_input: str,
|
|
120
|
+
*,
|
|
121
|
+
context: Any | None = None,
|
|
122
|
+
max_turns: int | None = 10,
|
|
123
|
+
hooks: Any | None = None,
|
|
124
|
+
) -> OpenAIAdapterResult:
|
|
125
|
+
try:
|
|
126
|
+
from agents import Runner
|
|
127
|
+
except Exception as exc: # noqa: BLE001 - optional adapter import boundary.
|
|
128
|
+
raise OpenAIAdapterUnavailable("openai-agents is not installed") from exc
|
|
129
|
+
result = await Runner.run(agent, user_input, context=context, max_turns=max_turns, hooks=hooks)
|
|
130
|
+
last_agent = getattr(getattr(result, "last_agent", None), "name", None)
|
|
131
|
+
return OpenAIAdapterResult(getattr(result, "final_output", None), last_agent, result)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class OpenAISemanticJudge:
|
|
135
|
+
def __init__(self, model: str = "gpt-5.5", api_key_env: str = "OPENAI_API_KEY") -> None:
|
|
136
|
+
self.model = model
|
|
137
|
+
self.api_key_env = api_key_env
|
|
138
|
+
|
|
139
|
+
async def judge(self, *, output: dict[str, Any], criterion: str) -> bool:
|
|
140
|
+
if not os.getenv(self.api_key_env):
|
|
141
|
+
raise OpenAIAdapterUnavailable(f"{self.api_key_env} is not set")
|
|
142
|
+
try:
|
|
143
|
+
from openai import AsyncOpenAI
|
|
144
|
+
except Exception as exc: # noqa: BLE001 - optional adapter import boundary.
|
|
145
|
+
raise OpenAIAdapterUnavailable("openai package is not installed") from exc
|
|
146
|
+
client = AsyncOpenAI()
|
|
147
|
+
response = await client.responses.create(
|
|
148
|
+
model=self.model,
|
|
149
|
+
input=[
|
|
150
|
+
{
|
|
151
|
+
"role": "system",
|
|
152
|
+
"content": "Return only PASS or FAIL. Evaluate whether the output satisfies the criterion.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"role": "user",
|
|
156
|
+
"content": f"Criterion: {criterion}\nOutput: {output}",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
)
|
|
160
|
+
text = getattr(response, "output_text", "")
|
|
161
|
+
return str(text).strip().upper() == "PASS"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _serializable(value: Any) -> Any:
|
|
165
|
+
if hasattr(value, "model_dump"):
|
|
166
|
+
return value.model_dump()
|
|
167
|
+
if isinstance(value, dict | list | str | int | float | bool) or value is None:
|
|
168
|
+
return value
|
|
169
|
+
return str(value)
|
contract4agents/ast.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""AST nodes for Contract4Agents source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class SourceSpan:
|
|
12
|
+
path: Path
|
|
13
|
+
line: int
|
|
14
|
+
column: int = 1
|
|
15
|
+
|
|
16
|
+
def display(self) -> str:
|
|
17
|
+
return f"{self.path}:{self.line}:{self.column}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class FieldDef:
|
|
22
|
+
name: str
|
|
23
|
+
type_name: str
|
|
24
|
+
nullable: bool = False
|
|
25
|
+
default: str | None = None
|
|
26
|
+
span: SourceSpan | None = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def normalized_type(self) -> str:
|
|
30
|
+
return self.type_name.rstrip("?").strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class TypeDef:
|
|
35
|
+
name: str
|
|
36
|
+
fields: list[FieldDef]
|
|
37
|
+
span: SourceSpan
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Permission = Literal["available", "preapproved", "requires_approval", "denied", "sandboxed"]
|
|
41
|
+
UseKind = Literal["tool", "agent", "datasource"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class UseDecl:
|
|
46
|
+
kind: UseKind
|
|
47
|
+
name: str
|
|
48
|
+
source: str
|
|
49
|
+
permission: Permission = "available"
|
|
50
|
+
span: SourceSpan | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class DatasourceDef:
|
|
55
|
+
name: str
|
|
56
|
+
python: str
|
|
57
|
+
requires: list[str]
|
|
58
|
+
produces: str
|
|
59
|
+
render: str = "markdown"
|
|
60
|
+
cache: str = "run"
|
|
61
|
+
span: SourceSpan | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class AgentDef:
|
|
66
|
+
name: str
|
|
67
|
+
parameters: list[FieldDef]
|
|
68
|
+
return_type: str
|
|
69
|
+
uses: list[UseDecl]
|
|
70
|
+
attributes: dict[str, Any]
|
|
71
|
+
span: SourceSpan
|
|
72
|
+
|
|
73
|
+
def list_attr(self, key: str) -> list[str]:
|
|
74
|
+
value = self.attributes.get(key, [])
|
|
75
|
+
return value if isinstance(value, list) else []
|
|
76
|
+
|
|
77
|
+
def text_attr(self, key: str) -> str:
|
|
78
|
+
value = self.attributes.get(key, "")
|
|
79
|
+
return value if isinstance(value, str) else ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class EvalCase:
|
|
84
|
+
name: str
|
|
85
|
+
agent: str
|
|
86
|
+
givens: dict[str, str]
|
|
87
|
+
expects: list[str]
|
|
88
|
+
semantic_expects: list[str]
|
|
89
|
+
span: SourceSpan
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class MonitorDef:
|
|
94
|
+
name: str
|
|
95
|
+
agent: str
|
|
96
|
+
severity: str
|
|
97
|
+
condition: str
|
|
98
|
+
expectation: str
|
|
99
|
+
span: SourceSpan
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ContractModule:
|
|
104
|
+
path: Path
|
|
105
|
+
types: list[TypeDef] = field(default_factory=list)
|
|
106
|
+
datasources: list[DatasourceDef] = field(default_factory=list)
|
|
107
|
+
agents: list[AgentDef] = field(default_factory=list)
|
|
108
|
+
evals: list[EvalCase] = field(default_factory=list)
|
|
109
|
+
monitors: list[MonitorDef] = field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class ContractProject:
|
|
114
|
+
root: Path
|
|
115
|
+
modules: list[ContractModule]
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def types(self) -> dict[str, TypeDef]:
|
|
119
|
+
return {item.name: item for module in self.modules for item in module.types}
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def datasources(self) -> dict[str, DatasourceDef]:
|
|
123
|
+
return {item.name: item for module in self.modules for item in module.datasources}
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def agents(self) -> dict[str, AgentDef]:
|
|
127
|
+
return {item.name: item for module in self.modules for item in module.agents}
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def evals(self) -> list[EvalCase]:
|
|
131
|
+
return [item for module in self.modules for item in module.evals]
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def monitors(self) -> list[MonitorDef]:
|
|
135
|
+
return [item for module in self.modules for item in module.monitors]
|
contract4agents/cli.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Click CLI for Contract4Agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from contract4agents.compiler import build_artifacts, compile_project
|
|
11
|
+
from contract4agents.diagnostics import ContractError, raise_if_errors
|
|
12
|
+
from contract4agents.docscheck import check_docs
|
|
13
|
+
from contract4agents.fixtures import FixtureReport, run_fixture_project_sync
|
|
14
|
+
from contract4agents.monitor import MonitorRule, run_monitors
|
|
15
|
+
from contract4agents.parser import parse_project
|
|
16
|
+
from contract4agents.runtime._trace_io import TraceFileError, load_trace_jsonl
|
|
17
|
+
from contract4agents.semantics import analyze_project
|
|
18
|
+
from contract4agents.visualization import build_visualization_graph, write_visualization_artifacts
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""Compile, validate, and evaluate Contract4Agents projects."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@main.command()
|
|
27
|
+
@click.argument("root", type=click.Path(path_type=Path), default=".", required=False)
|
|
28
|
+
def check(root: Path) -> None:
|
|
29
|
+
"""Parse and semantically validate a Contract4Agents project."""
|
|
30
|
+
try:
|
|
31
|
+
project = parse_project(root)
|
|
32
|
+
result = analyze_project(project)
|
|
33
|
+
for diagnostic in result.diagnostics:
|
|
34
|
+
click.echo(diagnostic.format(), err=diagnostic.severity == "error")
|
|
35
|
+
if not result.ok:
|
|
36
|
+
raise click.ClickException("Contract4Agents check failed")
|
|
37
|
+
click.echo("Contract4Agents check passed")
|
|
38
|
+
except ContractError as exc:
|
|
39
|
+
_print_contract_error(exc)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@main.command("compile")
|
|
43
|
+
@click.argument("root", type=click.Path(path_type=Path), default=".", required=False)
|
|
44
|
+
@click.option("--out", "output_dir", type=click.Path(path_type=Path), default=".contract/build")
|
|
45
|
+
@click.option("--check", "check_mode", is_flag=True, help="Fail if generated artifacts are stale.")
|
|
46
|
+
def compile_cmd(root: Path, output_dir: Path, check_mode: bool) -> None:
|
|
47
|
+
"""Compile a Contract4Agents project into provider-neutral artifacts."""
|
|
48
|
+
try:
|
|
49
|
+
compile_project(root, output_dir, check=check_mode)
|
|
50
|
+
click.echo("Contract4Agents compile passed")
|
|
51
|
+
except ContractError as exc:
|
|
52
|
+
_print_contract_error(exc)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@main.command("visualize")
|
|
56
|
+
@click.argument("root", type=click.Path(path_type=Path), default=".", required=False)
|
|
57
|
+
@click.option("--out", "output_dir", type=click.Path(path_type=Path), default=".contract/build/visualization")
|
|
58
|
+
def visualize_cmd(root: Path, output_dir: Path) -> None:
|
|
59
|
+
"""Generate static HTML visualization artifacts.
|
|
60
|
+
|
|
61
|
+
ROOT defaults to the current directory. The default output directory is
|
|
62
|
+
.contract/build/visualization.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
project = parse_project(root)
|
|
66
|
+
raise_if_errors(analyze_project(project).diagnostics)
|
|
67
|
+
artifacts = build_artifacts(project)
|
|
68
|
+
graph = build_visualization_graph(project, artifacts)
|
|
69
|
+
write_visualization_artifacts(graph, output_dir)
|
|
70
|
+
click.echo(f"Contract4Agents visualization written to {output_dir}")
|
|
71
|
+
except ContractError as exc:
|
|
72
|
+
_print_contract_error(exc)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@main.command("eval")
|
|
76
|
+
@click.argument("root", type=click.Path(path_type=Path), default=".", required=False)
|
|
77
|
+
def eval_cmd(root: Path) -> None:
|
|
78
|
+
"""Run local evals for a fixture.json project."""
|
|
79
|
+
if not (root / "fixture.json").exists():
|
|
80
|
+
raise click.ClickException("Contract4Agents eval requires ROOT/fixture.json")
|
|
81
|
+
try:
|
|
82
|
+
report = run_fixture_project_sync(project_root=root, run_root=root / ".contract" / "runs" / "last")
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
raise click.ClickException(f"Contract4Agents fixture eval failed: {exc}") from exc
|
|
85
|
+
_print_fixture_report(report)
|
|
86
|
+
if not report.passed:
|
|
87
|
+
raise click.ClickException("Contract4Agents eval failed")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _print_fixture_report(report: FixtureReport) -> None:
|
|
91
|
+
click.echo(f"Fixture eval {'passed' if report.passed else 'failed'}: {len(report.starts)} starts")
|
|
92
|
+
for start in report.starts:
|
|
93
|
+
status = "PASS" if start.passed and not start.monitor_violations else "FAIL"
|
|
94
|
+
click.echo(f"{status} {start.start_id}")
|
|
95
|
+
for failure in start.failures:
|
|
96
|
+
click.echo(f" {failure}")
|
|
97
|
+
for violation in start.monitor_violations:
|
|
98
|
+
click.echo(f" monitor: {violation}")
|
|
99
|
+
for skipped in start.skipped_semantic:
|
|
100
|
+
click.echo(f" semantic skipped: {skipped}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@main.command()
|
|
104
|
+
@click.argument("root", type=click.Path(path_type=Path), default=".", required=False)
|
|
105
|
+
@click.option("--trace", "trace_path", type=click.Path(path_type=Path), required=True, help="Trace JSONL file.")
|
|
106
|
+
def monitor(root: Path, trace_path: Path) -> None:
|
|
107
|
+
"""Run project monitors against a trace JSONL file."""
|
|
108
|
+
artifacts = compile_project(root)
|
|
109
|
+
try:
|
|
110
|
+
trace = load_trace_jsonl(trace_path)
|
|
111
|
+
except TraceFileError as exc:
|
|
112
|
+
raise click.ClickException(str(exc)) from exc
|
|
113
|
+
rules = [
|
|
114
|
+
MonitorRule(item["name"], item["agent"], item["severity"], item["when"], item["expect"])
|
|
115
|
+
for item in artifacts["monitors"]
|
|
116
|
+
]
|
|
117
|
+
violations = run_monitors(rules, trace)
|
|
118
|
+
for violation in violations:
|
|
119
|
+
click.echo(f"{violation.severity.upper()} {violation.rule}: {violation.message}")
|
|
120
|
+
if violations:
|
|
121
|
+
raise click.ClickException("Contract4Agents monitor failed")
|
|
122
|
+
click.echo("Contract4Agents monitor passed")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@main.command("docs-check")
|
|
126
|
+
@click.argument("root", type=click.Path(path_type=Path), default=".", required=False)
|
|
127
|
+
def docs_check(root: Path) -> None:
|
|
128
|
+
"""Check required documentation files and local markdown links.
|
|
129
|
+
|
|
130
|
+
ROOT defaults to the current directory.
|
|
131
|
+
"""
|
|
132
|
+
diagnostics = check_docs(root)
|
|
133
|
+
for diagnostic in diagnostics:
|
|
134
|
+
click.echo(diagnostic.format(), err=True)
|
|
135
|
+
if diagnostics:
|
|
136
|
+
raise click.ClickException("Docs check failed")
|
|
137
|
+
click.echo("Docs check passed")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _print_contract_error(exc: ContractError) -> None:
|
|
141
|
+
for diagnostic in exc.diagnostics:
|
|
142
|
+
click.echo(diagnostic.format(), err=True)
|
|
143
|
+
sys.exit(1)
|