chipgraph 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.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Packs: domain bundles of rules, roles, skills, checks, templates and commands.
|
|
2
|
+
|
|
3
|
+
A pack is a directory containing a `pack.yml` manifest. Core loads and validates the
|
|
4
|
+
manifest and checks that every path it advertises actually exists; it does not interpret
|
|
5
|
+
rules, skills or templates itself (that is packs' own concern, or later tasks').
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Literal
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
|
16
|
+
|
|
17
|
+
from chipgraph.core.plugin_api.registry import PluginError
|
|
18
|
+
|
|
19
|
+
_NAME_PATTERN = r"^[a-z0-9][a-z0-9-]*$"
|
|
20
|
+
_VERSION_PATTERN = r"^\d+\.\d+\.\d+$"
|
|
21
|
+
|
|
22
|
+
PackName = Annotated[str, StringConstraints(pattern=_NAME_PATTERN)]
|
|
23
|
+
PackVersion = Annotated[str, StringConstraints(pattern=_VERSION_PATTERN)]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PackRequires(BaseModel):
|
|
27
|
+
"""What a pack needs: a core version range, other packs, and capabilities."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
30
|
+
|
|
31
|
+
core: str = Field(default=">=0.0", description="Version range of chipgraph core required.")
|
|
32
|
+
packs: tuple[str, ...] = Field(default=(), description="Names of packs this pack requires.")
|
|
33
|
+
capabilities: tuple[str, ...] = Field(
|
|
34
|
+
default=(), description="Adapter capabilities this pack requires, e.g. 'lint'."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PackProvides(BaseModel):
|
|
39
|
+
"""What a pack provides: paths (relative to the pack directory) to its contents."""
|
|
40
|
+
|
|
41
|
+
# `schemas` is written `schema:` in pack.yml; the Python name avoids shadowing
|
|
42
|
+
# pydantic's deprecated BaseModel.schema().
|
|
43
|
+
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
|
|
44
|
+
|
|
45
|
+
rules: tuple[str, ...] = Field(default=(), description="Paths to rule definitions.")
|
|
46
|
+
agents: tuple[str, ...] = Field(default=(), description="Paths to agent role definitions.")
|
|
47
|
+
skills: tuple[str, ...] = Field(default=(), description="Paths to skill definitions.")
|
|
48
|
+
checks: tuple[str, ...] = Field(default=(), description="Paths to check definitions.")
|
|
49
|
+
workflows: tuple[str, ...] = Field(default=(), description="Paths to workflow definitions.")
|
|
50
|
+
templates: tuple[str, ...] = Field(default=(), description="Paths to templates.")
|
|
51
|
+
commands: tuple[str, ...] = Field(
|
|
52
|
+
default=(), description="Command names this pack provides, e.g. '/rtl'."
|
|
53
|
+
)
|
|
54
|
+
schemas: tuple[str, ...] = Field(
|
|
55
|
+
default=(),
|
|
56
|
+
alias="schema",
|
|
57
|
+
description="Paths to schema extensions (`schema:` in pack.yml).",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PackManifest(BaseModel):
|
|
62
|
+
"""The validated contents of a pack's `pack.yml`."""
|
|
63
|
+
|
|
64
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
65
|
+
|
|
66
|
+
schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
|
|
67
|
+
name: PackName = Field(description="The pack's name, e.g. 'digital-rtl'.")
|
|
68
|
+
version: PackVersion = Field(description="The pack's version, MAJOR.MINOR.PATCH.")
|
|
69
|
+
description: str = Field(default="", description="A short, plain-English description.")
|
|
70
|
+
requires: PackRequires = Field(
|
|
71
|
+
default_factory=PackRequires, description="What this pack needs."
|
|
72
|
+
)
|
|
73
|
+
provides: PackProvides = Field(
|
|
74
|
+
default_factory=PackProvides, description="What this pack provides."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Pack:
|
|
79
|
+
"""A loaded pack: its manifest, plus the directory it lives in."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, manifest: PackManifest, root: Path) -> None:
|
|
82
|
+
self.manifest = manifest
|
|
83
|
+
self.root = root
|
|
84
|
+
|
|
85
|
+
def path(self, rel: str) -> Path:
|
|
86
|
+
"""Resolve a path relative to this pack's directory."""
|
|
87
|
+
return self.root / rel
|
|
88
|
+
|
|
89
|
+
def __repr__(self) -> str:
|
|
90
|
+
return (
|
|
91
|
+
f"Pack(name={self.manifest.name!r}, "
|
|
92
|
+
f"version={self.manifest.version!r}, root={self.root!r})"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_PROVIDES_PATH_FIELDS = ("rules", "agents", "skills", "checks", "workflows", "templates", "schemas")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_pack(directory: Path) -> Pack:
|
|
100
|
+
"""Load and validate the pack at `directory`.
|
|
101
|
+
|
|
102
|
+
Raises `PluginError` if `pack.yml` is missing or invalid, or if any path listed in
|
|
103
|
+
`provides` (other than `commands`, which are names, not paths) does not exist under
|
|
104
|
+
`directory`.
|
|
105
|
+
"""
|
|
106
|
+
manifest_path = directory / "pack.yml"
|
|
107
|
+
if not manifest_path.is_file():
|
|
108
|
+
raise PluginError(f"no pack.yml found in {directory}")
|
|
109
|
+
try:
|
|
110
|
+
raw = yaml.safe_load(manifest_path.read_text()) or {}
|
|
111
|
+
manifest = PackManifest.model_validate(raw)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
raise PluginError(f"invalid pack manifest at {manifest_path}: {exc}") from exc
|
|
114
|
+
|
|
115
|
+
for field in _PROVIDES_PATH_FIELDS:
|
|
116
|
+
for rel in getattr(manifest.provides, field):
|
|
117
|
+
if not (directory / rel).exists():
|
|
118
|
+
raise PluginError(
|
|
119
|
+
f"pack {manifest.name!r} provides missing path {rel!r} "
|
|
120
|
+
f"(expected at {directory / rel})"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return Pack(manifest, directory)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def discover_packs(search_paths: Iterable[Path]) -> dict[str, Pack]:
|
|
127
|
+
"""Load every pack found as an immediate subdirectory (containing `pack.yml`) of any path.
|
|
128
|
+
|
|
129
|
+
Raises `PluginError` if two discovered packs share a name.
|
|
130
|
+
"""
|
|
131
|
+
packs: dict[str, Pack] = {}
|
|
132
|
+
for search_path in search_paths:
|
|
133
|
+
if not search_path.is_dir():
|
|
134
|
+
continue
|
|
135
|
+
for entry in sorted(search_path.iterdir()):
|
|
136
|
+
if not entry.is_dir() or not (entry / "pack.yml").is_file():
|
|
137
|
+
continue
|
|
138
|
+
pack = load_pack(entry)
|
|
139
|
+
name = pack.manifest.name
|
|
140
|
+
if name in packs:
|
|
141
|
+
raise PluginError(
|
|
142
|
+
f"duplicate pack name {name!r}: found at {packs[name].root} and {entry}"
|
|
143
|
+
)
|
|
144
|
+
packs[name] = pack
|
|
145
|
+
return packs
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def resolve_requires(packs: Mapping[str, Pack]) -> None:
|
|
149
|
+
"""Check that every pack's `requires.packs` is present in `packs`.
|
|
150
|
+
|
|
151
|
+
Raises `PluginError` naming the first missing dependency found.
|
|
152
|
+
"""
|
|
153
|
+
for pack in packs.values():
|
|
154
|
+
for required in pack.manifest.requires.packs:
|
|
155
|
+
if required not in packs:
|
|
156
|
+
raise PluginError(
|
|
157
|
+
f"pack {pack.manifest.name!r} requires pack {required!r}, "
|
|
158
|
+
"which is not available"
|
|
159
|
+
)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Plugin protocols: the structural interfaces adapters, checks and packs implement.
|
|
2
|
+
|
|
3
|
+
Every protocol here is `@runtime_checkable`, so `isinstance(obj, Protocol)` works for
|
|
4
|
+
registry validation (see `registry.py`). Each protocol carries a `name: str` attribute
|
|
5
|
+
identifying the concrete implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Protocol, runtime_checkable
|
|
13
|
+
|
|
14
|
+
from chipgraph.core.contracts import AgentResult, Approval, CheckResult, CheckSpec, Issue
|
|
15
|
+
from chipgraph.core.plugin_api.types import (
|
|
16
|
+
AgentTask,
|
|
17
|
+
LlmRequest,
|
|
18
|
+
LlmResponse,
|
|
19
|
+
RunResult,
|
|
20
|
+
ToolContext,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class Runner(Protocol):
|
|
26
|
+
"""Runs subprocess commands. Core never calls `subprocess` directly; it goes through this."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
|
|
30
|
+
async def run(
|
|
31
|
+
self,
|
|
32
|
+
cmd: Sequence[str],
|
|
33
|
+
*,
|
|
34
|
+
cwd: Path,
|
|
35
|
+
env: Mapping[str, str] | None = None,
|
|
36
|
+
timeout_s: float | None = None,
|
|
37
|
+
) -> RunResult:
|
|
38
|
+
"""Run `cmd` in `cwd` and return its result."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class LogParser(Protocol):
|
|
44
|
+
"""Parses a tool's raw log text into structured issues."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
|
|
48
|
+
def parse(self, log: str) -> tuple[Issue, ...]:
|
|
49
|
+
"""Extract issues from `log`."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@runtime_checkable
|
|
54
|
+
class ToolAdapter(Protocol):
|
|
55
|
+
"""Wraps an external EDA tool (or other command-line tool) behind a common interface."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
capability: str
|
|
59
|
+
|
|
60
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
61
|
+
"""Run the check described by `spec` under `ctx` and return its result."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@runtime_checkable
|
|
66
|
+
class Check(Protocol):
|
|
67
|
+
"""A built-in, deterministic check that needs no external tool."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
id: str
|
|
71
|
+
|
|
72
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
73
|
+
"""Run this check for `spec` under `ctx` and return its result."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@runtime_checkable
|
|
78
|
+
class VcsAdapter(Protocol):
|
|
79
|
+
"""Version control operations the engine needs: head, diffs, and disposable workspaces."""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
|
|
83
|
+
def head(self, root: Path) -> str:
|
|
84
|
+
"""Return the current head revision of the repo at `root`."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
def changed_files(self, root: Path, since: str | None = None) -> tuple[str, ...]:
|
|
88
|
+
"""Return paths changed since `since` (or since the last commit, if None)."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def create_workspace(self, root: Path, name: str) -> Path:
|
|
92
|
+
"""Create a disposable workspace (e.g. a worktree) named `name` and return its path."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def remove_workspace(self, path: Path) -> None:
|
|
96
|
+
"""Remove a workspace previously created by `create_workspace`."""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@runtime_checkable
|
|
101
|
+
class ReviewAdapter(Protocol):
|
|
102
|
+
"""Records and retrieves human approvals against gates."""
|
|
103
|
+
|
|
104
|
+
name: str
|
|
105
|
+
|
|
106
|
+
def record(self, approval: Approval) -> None:
|
|
107
|
+
"""Persist `approval`."""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def approvals(self, gate_id: str) -> tuple[Approval, ...]:
|
|
111
|
+
"""Return every approval recorded against `gate_id`."""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@runtime_checkable
|
|
116
|
+
class LlmProvider(Protocol):
|
|
117
|
+
"""Completes LLM requests. `local` gates whether it may see data labeled 'nda'."""
|
|
118
|
+
|
|
119
|
+
name: str
|
|
120
|
+
local: bool
|
|
121
|
+
"""True when this provider is self-hosted; only then may it receive 'nda'-labeled data."""
|
|
122
|
+
|
|
123
|
+
async def complete(self, request: LlmRequest) -> LlmResponse:
|
|
124
|
+
"""Complete `request` and return the response."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@runtime_checkable
|
|
129
|
+
class AgentRuntime(Protocol):
|
|
130
|
+
"""Runs an `AgentTask` to completion (or to a stopping point) via some agent harness."""
|
|
131
|
+
|
|
132
|
+
name: str
|
|
133
|
+
|
|
134
|
+
async def run_task(self, task: AgentTask) -> AgentResult:
|
|
135
|
+
"""Run `task` and return its result."""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@runtime_checkable
|
|
140
|
+
class FormatAdapter(Protocol):
|
|
141
|
+
"""Loads a domain file format into raw facts.
|
|
142
|
+
|
|
143
|
+
Returns raw facts (plain mappings), not the typed Design Model: the typed model
|
|
144
|
+
arrives in M1-01. Callers that need the Design Model translate these facts themselves.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
name: str
|
|
148
|
+
|
|
149
|
+
def load(self, path: Path) -> Iterable[Mapping[str, object]]:
|
|
150
|
+
"""Load `path` and yield its facts."""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@runtime_checkable
|
|
155
|
+
class Extractor(Protocol):
|
|
156
|
+
"""Extracts facts from a source file (e.g. RTL) by parsing it.
|
|
157
|
+
|
|
158
|
+
Returns raw facts (plain mappings), not the typed Design Model: the typed model
|
|
159
|
+
arrives in M1-01. Callers that need the Design Model translate these facts themselves.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
name: str
|
|
163
|
+
|
|
164
|
+
def extract(self, path: Path) -> Iterable[Mapping[str, object]]:
|
|
165
|
+
"""Extract facts from `path`."""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@runtime_checkable
|
|
170
|
+
class Generator(Protocol):
|
|
171
|
+
"""Generates output files from facts, e.g. rendering a template."""
|
|
172
|
+
|
|
173
|
+
name: str
|
|
174
|
+
|
|
175
|
+
def generate(self, out_dir: Path, facts: Iterable[Mapping[str, object]]) -> tuple[Path, ...]:
|
|
176
|
+
"""Write generated files under `out_dir` from `facts` and return their paths."""
|
|
177
|
+
...
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""The plugin registry: where adapters, checks and other plugins register themselves.
|
|
2
|
+
|
|
3
|
+
Plugins register either directly (`Registry.register`) or by Python entry points
|
|
4
|
+
(`Registry.discover`), under the group `chipgraph.adapters.<kind>` for each kind in `KINDS`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from importlib.metadata import entry_points
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from chipgraph.core.plugin_api.protocols import (
|
|
14
|
+
AgentRuntime,
|
|
15
|
+
Check,
|
|
16
|
+
Extractor,
|
|
17
|
+
FormatAdapter,
|
|
18
|
+
Generator,
|
|
19
|
+
LlmProvider,
|
|
20
|
+
LogParser,
|
|
21
|
+
ReviewAdapter,
|
|
22
|
+
Runner,
|
|
23
|
+
ToolAdapter,
|
|
24
|
+
VcsAdapter,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
KINDS: dict[str, type] = {
|
|
28
|
+
"runner": Runner,
|
|
29
|
+
"parser": LogParser,
|
|
30
|
+
"tool": ToolAdapter,
|
|
31
|
+
"check": Check,
|
|
32
|
+
"vcs": VcsAdapter,
|
|
33
|
+
"review": ReviewAdapter,
|
|
34
|
+
"llm": LlmProvider,
|
|
35
|
+
"runtime": AgentRuntime,
|
|
36
|
+
"format": FormatAdapter,
|
|
37
|
+
"extractor": Extractor,
|
|
38
|
+
"generator": Generator,
|
|
39
|
+
}
|
|
40
|
+
"""Maps a plugin kind name to the protocol it must satisfy."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PluginError(Exception):
|
|
44
|
+
"""Raised for any registry or pack error: unknown kind, duplicate name, bad plugin, ..."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Registry:
|
|
48
|
+
"""Holds registered plugin instances, keyed by kind and name."""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._plugins: dict[str, dict[str, object]] = {kind: {} for kind in KINDS}
|
|
52
|
+
|
|
53
|
+
def register(self, kind: str, name: str, obj: object) -> None:
|
|
54
|
+
"""Register `obj` under `kind`/`name`.
|
|
55
|
+
|
|
56
|
+
Raises `PluginError` if `kind` is unknown, `name` is already registered for that
|
|
57
|
+
kind, or `obj` does not satisfy the protocol for `kind`.
|
|
58
|
+
"""
|
|
59
|
+
if kind not in KINDS:
|
|
60
|
+
raise PluginError(f"unknown plugin kind {kind!r}; known kinds: {sorted(KINDS)}")
|
|
61
|
+
if name in self._plugins[kind]:
|
|
62
|
+
raise PluginError(f"{kind}/{name} is already registered")
|
|
63
|
+
protocol = KINDS[kind]
|
|
64
|
+
if not isinstance(obj, protocol):
|
|
65
|
+
raise PluginError(
|
|
66
|
+
f"{obj!r} does not satisfy the {protocol.__name__} protocol for kind {kind!r}"
|
|
67
|
+
)
|
|
68
|
+
self._plugins[kind][name] = obj
|
|
69
|
+
|
|
70
|
+
def get(self, kind: str, name: str) -> Any:
|
|
71
|
+
"""Return the plugin registered under `kind`/`name`.
|
|
72
|
+
|
|
73
|
+
Raises `PluginError` if `kind` is unknown, listing the registered names otherwise.
|
|
74
|
+
"""
|
|
75
|
+
if kind not in KINDS:
|
|
76
|
+
raise PluginError(f"unknown plugin kind {kind!r}; known kinds: {sorted(KINDS)}")
|
|
77
|
+
try:
|
|
78
|
+
return self._plugins[kind][name]
|
|
79
|
+
except KeyError:
|
|
80
|
+
available = sorted(self._plugins[kind])
|
|
81
|
+
raise PluginError(f"no {kind} plugin named {name!r}; available: {available}") from None
|
|
82
|
+
|
|
83
|
+
def names(self, kind: str) -> tuple[str, ...]:
|
|
84
|
+
"""Return the sorted names registered under `kind`."""
|
|
85
|
+
if kind not in KINDS:
|
|
86
|
+
raise PluginError(f"unknown plugin kind {kind!r}; known kinds: {sorted(KINDS)}")
|
|
87
|
+
return tuple(sorted(self._plugins[kind]))
|
|
88
|
+
|
|
89
|
+
def discover(self) -> int:
|
|
90
|
+
"""Load plugins from Python entry points, one group per kind.
|
|
91
|
+
|
|
92
|
+
For each kind, entry points under group `chipgraph.adapters.<kind>` are loaded.
|
|
93
|
+
A loaded object that is a class or a no-argument factory is called to produce the
|
|
94
|
+
plugin instance; anything else is registered as-is. Returns the number registered.
|
|
95
|
+
Any error loading or registering one entry point becomes a `PluginError` naming it.
|
|
96
|
+
"""
|
|
97
|
+
count = 0
|
|
98
|
+
for kind in KINDS:
|
|
99
|
+
group = f"chipgraph.adapters.{kind}"
|
|
100
|
+
for entry_point in entry_points(group=group):
|
|
101
|
+
try:
|
|
102
|
+
loaded = entry_point.load()
|
|
103
|
+
is_factory = inspect.isclass(loaded) or inspect.isfunction(loaded)
|
|
104
|
+
obj = loaded() if is_factory else loaded
|
|
105
|
+
self.register(kind, entry_point.name, obj)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
raise PluginError(
|
|
108
|
+
f"failed to load entry point {entry_point.name!r} in group {group!r}: {exc}"
|
|
109
|
+
) from exc
|
|
110
|
+
count += 1
|
|
111
|
+
return count
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Shared plugin types: in-process data passed to and from plugin protocols.
|
|
2
|
+
|
|
3
|
+
These models are not persisted (no `schema_version`) and are not part of the artifact
|
|
4
|
+
store; they only carry data between core and adapters/packs during a single run.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from chipgraph.core.contracts import Budget, DataLabel, RuleInstance
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from chipgraph.core.plugin_api.protocols import Runner
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunResult(BaseModel):
|
|
21
|
+
"""The outcome of running a subprocess command via a `Runner`."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
24
|
+
|
|
25
|
+
returncode: int = Field(description="The process's exit code.")
|
|
26
|
+
stdout: str = Field(description="Captured standard output.")
|
|
27
|
+
stderr: str = Field(description="Captured standard error.")
|
|
28
|
+
duration_s: float = Field(ge=0, description="How long the command took, in seconds.")
|
|
29
|
+
timed_out: bool = Field(default=False, description="Whether the command was killed on timeout.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ToolContext(BaseModel):
|
|
33
|
+
"""Context a `ToolAdapter` or `Check` runs with: repo, runner, env, and free params."""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(extra="forbid", frozen=True, arbitrary_types_allowed=True)
|
|
36
|
+
|
|
37
|
+
repo_root: Path = Field(description="Root of the repo (or workspace member) being worked on.")
|
|
38
|
+
runner: Runner = Field(description="The subprocess runner to use for any command.")
|
|
39
|
+
env: dict[str, str] = Field(default={}, description="Extra environment variables.")
|
|
40
|
+
params: dict[str, str] = Field(
|
|
41
|
+
default={}, description="Free-form parameters, e.g. block, module."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LlmMessage(BaseModel):
|
|
46
|
+
"""A single message in an LLM conversation."""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
49
|
+
|
|
50
|
+
role: Literal["system", "user", "assistant"] = Field(description="Who sent this message.")
|
|
51
|
+
content: str = Field(description="The message text.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LlmRequest(BaseModel):
|
|
55
|
+
"""A request to complete a conversation with an LLM."""
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
58
|
+
|
|
59
|
+
model: str = Field(description="The model id to use.")
|
|
60
|
+
messages: tuple[LlmMessage, ...] = Field(min_length=1, description="The conversation so far.")
|
|
61
|
+
max_tokens: int = Field(ge=1, description="Maximum number of tokens to generate.")
|
|
62
|
+
labels: tuple[DataLabel, ...] = Field(
|
|
63
|
+
default=(),
|
|
64
|
+
description=(
|
|
65
|
+
"Data labels of everything in the context. A provider must refuse a request "
|
|
66
|
+
"carrying 'nda' unless it is local (see LlmProvider.local)."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LlmResponse(BaseModel):
|
|
72
|
+
"""The result of completing an LLM request."""
|
|
73
|
+
|
|
74
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
75
|
+
|
|
76
|
+
text: str = Field(description="The generated text.")
|
|
77
|
+
input_tokens: int = Field(ge=0, description="Tokens consumed by the input.")
|
|
78
|
+
output_tokens: int = Field(ge=0, description="Tokens consumed by the output.")
|
|
79
|
+
model: str = Field(description="The model id that actually served the request.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AgentTask(BaseModel):
|
|
83
|
+
"""A single task handed to an `AgentRuntime`: what to do, and under what constraints."""
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
86
|
+
|
|
87
|
+
instance: RuleInstance = Field(description="The rule instance this task fulfils.")
|
|
88
|
+
role: str = Field(description="The agent role that runs this task.")
|
|
89
|
+
skills: tuple[str, ...] = Field(default=(), description="Skill ids available to the agent.")
|
|
90
|
+
allowed_writes: tuple[str, ...] = Field(
|
|
91
|
+
min_length=1, description="Repo-relative paths the agent is allowed to write."
|
|
92
|
+
)
|
|
93
|
+
context: dict[str, str] = Field(default={}, description="Free-form context for the agent.")
|
|
94
|
+
budget: Budget = Field(description="Resource budget for this task.")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Agent runtime state: the task queue an out-of-process agent runtime works from, and
|
|
2
|
+
the executor that hands agent rules to an `AgentRuntime` (DESIGN.md 5.2, 5.5; D35). The
|
|
3
|
+
roles and skills, as data, are in `chipgraph.core.runtime.roles` (DESIGN.md 5.1; D8).
|
|
4
|
+
|
|
5
|
+
`chipgraph.core.runtime` is generic: it knows nothing about any agent harness, model or
|
|
6
|
+
tool. Runtimes live in `chipgraph.adapters.runtime`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from chipgraph.core.runtime.executor import (
|
|
10
|
+
AgentRuntimeExecutor,
|
|
11
|
+
instance_inputs_hash,
|
|
12
|
+
outcome_for,
|
|
13
|
+
task_for,
|
|
14
|
+
)
|
|
15
|
+
from chipgraph.core.runtime.queue import (
|
|
16
|
+
ACTIVE_STATUSES,
|
|
17
|
+
DEFAULT_TOOL_CALL_CAP,
|
|
18
|
+
OPEN_STATUSES,
|
|
19
|
+
AgentBinding,
|
|
20
|
+
AgentTaskRecord,
|
|
21
|
+
QueueError,
|
|
22
|
+
TaskQueue,
|
|
23
|
+
TaskStatus,
|
|
24
|
+
state_key,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ACTIVE_STATUSES",
|
|
29
|
+
"DEFAULT_TOOL_CALL_CAP",
|
|
30
|
+
"OPEN_STATUSES",
|
|
31
|
+
"AgentBinding",
|
|
32
|
+
"AgentRuntimeExecutor",
|
|
33
|
+
"AgentTaskRecord",
|
|
34
|
+
"QueueError",
|
|
35
|
+
"TaskQueue",
|
|
36
|
+
"TaskStatus",
|
|
37
|
+
"instance_inputs_hash",
|
|
38
|
+
"outcome_for",
|
|
39
|
+
"state_key",
|
|
40
|
+
"task_for",
|
|
41
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""`AgentRuntimeExecutor`: runs `kind: agent` rules through an `AgentRuntime` (DESIGN.md 5.2).
|
|
2
|
+
|
|
3
|
+
The scheduler knows `Executor`s; agent runtimes implement `AgentRuntime.run_task`. This
|
|
4
|
+
executor turns a rule instance into an `AgentTask` (role, skills, budget, and the
|
|
5
|
+
instance's concrete outputs as the only files the agent may write) and maps the
|
|
6
|
+
runtime's `AgentResult` back to an `ExecOutcome`:
|
|
7
|
+
|
|
8
|
+
- `done` -> ok; the scheduler then runs the rule's checks and records the outputs.
|
|
9
|
+
- `needs_human` -> not ok (`planning`): the open questions are the message. A runtime
|
|
10
|
+
that runs outside the engine uses this for "the task is waiting for an agent" too.
|
|
11
|
+
- `budget_exhausted` / `failed` -> not ok (`verification`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from chipgraph.core.contracts import AgentResult, RuleInstance, RuleSpec
|
|
17
|
+
from chipgraph.core.contracts.types import FailureLabel
|
|
18
|
+
from chipgraph.core.engine.scheduler import ExecOutcome
|
|
19
|
+
from chipgraph.core.plugin_api.protocols import AgentRuntime
|
|
20
|
+
from chipgraph.core.plugin_api.types import AgentTask
|
|
21
|
+
from chipgraph.core.state.artifacts import ArtifactStore, hash_inputs
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def instance_inputs_hash(store: ArtifactStore, instance: RuleInstance) -> str:
|
|
25
|
+
"""A hash over the instance's inputs, the same way the scheduler's records hash them.
|
|
26
|
+
|
|
27
|
+
File inputs hash their content (a missing file counts as empty); model inputs are
|
|
28
|
+
keyed by their model key only, since the Design Model hashes its own entities.
|
|
29
|
+
"""
|
|
30
|
+
path_refs = [ref for ref in instance.inputs if ref.path is not None]
|
|
31
|
+
current = store.current_hashes(path_refs)
|
|
32
|
+
pairs = []
|
|
33
|
+
for ref in instance.inputs:
|
|
34
|
+
locator = ref.path if ref.path is not None else ref.model_key
|
|
35
|
+
key = f"{ref.repo}:{locator}"
|
|
36
|
+
pairs.append((key, current.get(key, "")))
|
|
37
|
+
return hash_inputs(pairs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def task_for(rule: RuleSpec, instance: RuleInstance) -> AgentTask:
|
|
41
|
+
"""The `AgentTask` for one instance of an agent rule."""
|
|
42
|
+
if rule.role is None:
|
|
43
|
+
raise ValueError(f"rule {rule.id!r} has no role")
|
|
44
|
+
writes = tuple(ref.path for ref in instance.outputs if ref.path is not None)
|
|
45
|
+
return AgentTask(
|
|
46
|
+
instance=instance,
|
|
47
|
+
role=rule.role,
|
|
48
|
+
skills=rule.skills,
|
|
49
|
+
allowed_writes=writes,
|
|
50
|
+
budget=rule.budget,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def outcome_for(result: AgentResult, *, task_id: str) -> ExecOutcome:
|
|
55
|
+
"""Map an `AgentResult` to the scheduler's `ExecOutcome`."""
|
|
56
|
+
if result.status == "done":
|
|
57
|
+
return ExecOutcome(ok=True)
|
|
58
|
+
label: FailureLabel = "planning" if result.status == "needs_human" else "verification"
|
|
59
|
+
detail = "; ".join(result.open_questions)
|
|
60
|
+
default = f"agent task {task_id!r} ended with status {result.status!r}"
|
|
61
|
+
return ExecOutcome(ok=False, failure_label=label, message=detail or default)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AgentRuntimeExecutor:
|
|
65
|
+
"""An `Executor` for `kind: agent` rules, backed by an `AgentRuntime`."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, runtime: AgentRuntime) -> None:
|
|
68
|
+
self.runtime = runtime
|
|
69
|
+
|
|
70
|
+
async def execute(self, rule: RuleSpec, instance: RuleInstance) -> ExecOutcome:
|
|
71
|
+
try:
|
|
72
|
+
task = task_for(rule, instance)
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
return ExecOutcome(ok=False, failure_label="planning", message=str(exc))
|
|
75
|
+
result = await self.runtime.run_task(task)
|
|
76
|
+
return outcome_for(result, task_id=instance.instance_id)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["AgentRuntimeExecutor", "instance_inputs_hash", "outcome_for", "task_for"]
|