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,411 @@
|
|
|
1
|
+
"""HANDOFF.md and the status summary (DESIGN.md 6.2).
|
|
2
|
+
|
|
3
|
+
Every agent session starts by reading ``HANDOFF.md``: what was done, who is waited on,
|
|
4
|
+
open questions, and the next step. This module derives that summary — and the compact
|
|
5
|
+
``chipgraph status`` table — purely from a run's journal events, using
|
|
6
|
+
:func:`chipgraph.core.state.journal.replay` for per-instance status and reading messages,
|
|
7
|
+
gate ids and open questions straight out of the event payloads the scheduler writes
|
|
8
|
+
(``core/engine/scheduler.py``).
|
|
9
|
+
|
|
10
|
+
Payload keys relied on (as written by ``Scheduler._process_instance``/``_execute``):
|
|
11
|
+
|
|
12
|
+
- ``rule_done``: ``payload["skipped"] == "fresh"`` marks a fresh (not rebuilt) instance.
|
|
13
|
+
- ``rule_fail``: ``payload["message"]`` is the human-readable failure message;
|
|
14
|
+
``event.failure_label`` carries the :class:`FailureLabel`.
|
|
15
|
+
- ``gate_wait``: ``payload["gate"]`` is the (already-formatted) gate id.
|
|
16
|
+
- ``rule_fail``/``agent_turn``: an optional ``payload["open_questions"]`` list of strings.
|
|
17
|
+
- ``rule_fail`` of an agent rule (``core/engine/agent_rule.py``): ``payload["agent"]``, the
|
|
18
|
+
loop's summary (status, tries, max_tries, infra_failures, reason, label, failed_checks,
|
|
19
|
+
failures, tokens, cost, stopped_earlier); read into :class:`AgentStopInfo` so HANDOFF.md
|
|
20
|
+
says how many tries were used, why the loop stopped and what failed last.
|
|
21
|
+
- ``run_stop``: only carries *counts* today (``done``, ``failed``, ...), not instance ids.
|
|
22
|
+
The scheduler currently emits no per-instance event for a *blocked* instance (one whose
|
|
23
|
+
dependency failed/waited/was itself blocked), so ``build_handoff`` reads blocked instance
|
|
24
|
+
ids from an optional ``payload["blocked"]`` list on the last ``run_stop`` event if present,
|
|
25
|
+
and otherwise reports no blocked instances. This is forward-compatible with the scheduler
|
|
26
|
+
later being extended to record that list; it is not a change to the scheduler itself.
|
|
27
|
+
|
|
28
|
+
A HANDOFF.md is a snapshot of the run that wrote it. When an agent task stops, the file is
|
|
29
|
+
written at once, for the run that dispatched the task's last try; a task accepted after that
|
|
30
|
+
still shows as waiting there. The next ``next_task`` or ``build`` writes its own, accurate one.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
from collections.abc import Mapping, Sequence
|
|
37
|
+
from datetime import UTC, datetime
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, ValidationError
|
|
42
|
+
|
|
43
|
+
from chipgraph.core.contracts.event import Event
|
|
44
|
+
from chipgraph.core.contracts.types import FailureLabel
|
|
45
|
+
from chipgraph.core.state.journal import RunState, replay
|
|
46
|
+
from chipgraph.core.state.layout import StateLayout
|
|
47
|
+
|
|
48
|
+
_FAILURE_NEXT_STEP: dict[FailureLabel, str] = {
|
|
49
|
+
"constraint": "revert the hand edit or approve it, then `chipgraph build {target}`",
|
|
50
|
+
"context": "add the missing input or output, then `chipgraph resume {run_id}`",
|
|
51
|
+
"verification": "fix the failing check, then `chipgraph resume {run_id}`",
|
|
52
|
+
"infra": "check the tool or connection (`chipgraph doctor`), then `chipgraph resume {run_id}`",
|
|
53
|
+
"planning": "answer the open questions, then `chipgraph resume {run_id}`",
|
|
54
|
+
}
|
|
55
|
+
_DEFAULT_NEXT_STEP = "`chipgraph resume {run_id}`"
|
|
56
|
+
_AGENT_STEP_KEY = {"needs_human": "needs_human", "planning": "needs_human", "infra": "infra"}
|
|
57
|
+
"""An agent stop reason -> its `_AGENT_NEXT_STEP` key; any other reason is `exhausted`."""
|
|
58
|
+
_AGENT_NEXT_STEP = {
|
|
59
|
+
"needs_human": (
|
|
60
|
+
"answer the open questions for `{instance}` (update its inputs), then "
|
|
61
|
+
"`chipgraph build {target}`"
|
|
62
|
+
),
|
|
63
|
+
"infra": (
|
|
64
|
+
"check the tool or connection (`chipgraph doctor`), then `chipgraph rewind {instance}` "
|
|
65
|
+
"and `chipgraph build {target}`: the stop is remembered until the instance is rewound "
|
|
66
|
+
"or an input changes, so `resume` would not run it again"
|
|
67
|
+
),
|
|
68
|
+
"exhausted": (
|
|
69
|
+
"read the last failures of `{instance}`; fix the cause (its inputs, the rule or "
|
|
70
|
+
"its budget) or write the output by hand, then `chipgraph rewind {instance}` and "
|
|
71
|
+
"`chipgraph build {target}`"
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
_NOTHING_TO_DO = "nothing to do: the target is up to date."
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AgentStopInfo(BaseModel):
|
|
78
|
+
"""How an agent rule's loop stopped, from its ``rule_fail`` payload's ``agent`` key."""
|
|
79
|
+
|
|
80
|
+
model_config = ConfigDict(frozen=True, extra="ignore")
|
|
81
|
+
|
|
82
|
+
status: str = ""
|
|
83
|
+
reason: str | None = None
|
|
84
|
+
tries: int = 0
|
|
85
|
+
max_tries: int = 0
|
|
86
|
+
infra_failures: int = 0
|
|
87
|
+
label: FailureLabel | None = None
|
|
88
|
+
failed_checks: tuple[str, ...] = ()
|
|
89
|
+
failures: tuple[str, ...] = ()
|
|
90
|
+
tokens: int | None = None
|
|
91
|
+
cost: float | None = None
|
|
92
|
+
stopped_earlier: bool = False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class FailedItem(BaseModel):
|
|
96
|
+
"""One failed rule instance."""
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
99
|
+
|
|
100
|
+
instance: str
|
|
101
|
+
label: FailureLabel | None = None
|
|
102
|
+
message: str = ""
|
|
103
|
+
agent: AgentStopInfo | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _agent_info(payload: dict[str, Any]) -> AgentStopInfo | None:
|
|
107
|
+
raw = payload.get("agent")
|
|
108
|
+
if not isinstance(raw, dict):
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
return AgentStopInfo.model_validate(raw)
|
|
112
|
+
except ValidationError:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class WaitingItem(BaseModel):
|
|
117
|
+
"""One rule instance waiting on a human gate."""
|
|
118
|
+
|
|
119
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
120
|
+
|
|
121
|
+
instance: str
|
|
122
|
+
gate_id: str
|
|
123
|
+
who: tuple[str, ...] = ()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Handoff(BaseModel):
|
|
127
|
+
"""Everything the next agent session needs on resume (DESIGN.md 6.2)."""
|
|
128
|
+
|
|
129
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
130
|
+
|
|
131
|
+
run_id: str
|
|
132
|
+
target: str
|
|
133
|
+
generated_at: AwareDatetime
|
|
134
|
+
done: tuple[str, ...] = ()
|
|
135
|
+
skipped_fresh: tuple[str, ...] = ()
|
|
136
|
+
failed: tuple[FailedItem, ...] = ()
|
|
137
|
+
waiting_gate: tuple[WaitingItem, ...] = ()
|
|
138
|
+
blocked: tuple[str, ...] = ()
|
|
139
|
+
open_questions: tuple[str, ...] = ()
|
|
140
|
+
next_steps: tuple[str, ...] = ()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _collect_open_questions(events: Sequence[Event]) -> tuple[str, ...]:
|
|
144
|
+
seen: set[str] = set()
|
|
145
|
+
questions: list[str] = []
|
|
146
|
+
for event in events:
|
|
147
|
+
if event.type not in ("rule_fail", "agent_turn"):
|
|
148
|
+
continue
|
|
149
|
+
raw = event.payload.get("open_questions")
|
|
150
|
+
if not isinstance(raw, list):
|
|
151
|
+
continue
|
|
152
|
+
for item in raw:
|
|
153
|
+
text = str(item)
|
|
154
|
+
if text not in seen:
|
|
155
|
+
seen.add(text)
|
|
156
|
+
questions.append(text)
|
|
157
|
+
return tuple(questions)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _next_steps(
|
|
161
|
+
*,
|
|
162
|
+
target: str,
|
|
163
|
+
run_id: str,
|
|
164
|
+
failed: tuple[FailedItem, ...],
|
|
165
|
+
waiting_gate: tuple[WaitingItem, ...],
|
|
166
|
+
blocked: tuple[str, ...],
|
|
167
|
+
) -> tuple[str, ...]:
|
|
168
|
+
steps: list[str] = []
|
|
169
|
+
for wait_item in waiting_gate:
|
|
170
|
+
steps.append(f"`chipgraph approve {wait_item.gate_id} --instance {wait_item.instance}`")
|
|
171
|
+
for fail_item in failed:
|
|
172
|
+
if fail_item.agent is not None and fail_item.agent.reason is not None:
|
|
173
|
+
key = _AGENT_STEP_KEY.get(fail_item.agent.reason, "exhausted")
|
|
174
|
+
template = _AGENT_NEXT_STEP[key]
|
|
175
|
+
elif fail_item.label is not None:
|
|
176
|
+
template = _FAILURE_NEXT_STEP.get(fail_item.label, _DEFAULT_NEXT_STEP)
|
|
177
|
+
else:
|
|
178
|
+
template = _DEFAULT_NEXT_STEP
|
|
179
|
+
steps.append(template.format(target=target, run_id=run_id, instance=fail_item.instance))
|
|
180
|
+
|
|
181
|
+
if not steps and not blocked:
|
|
182
|
+
return (_NOTHING_TO_DO,)
|
|
183
|
+
|
|
184
|
+
seen: set[str] = set()
|
|
185
|
+
deduped: list[str] = []
|
|
186
|
+
for step in steps:
|
|
187
|
+
if step not in seen:
|
|
188
|
+
seen.add(step)
|
|
189
|
+
deduped.append(step)
|
|
190
|
+
return tuple(deduped)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_handoff(
|
|
194
|
+
events: Sequence[Event],
|
|
195
|
+
*,
|
|
196
|
+
target: str,
|
|
197
|
+
approvers: Mapping[str, Sequence[str]] | None = None,
|
|
198
|
+
now: datetime | None = None,
|
|
199
|
+
) -> Handoff:
|
|
200
|
+
"""Derive a :class:`Handoff` from a run's journal events.
|
|
201
|
+
|
|
202
|
+
``approvers`` maps a gate id to the people known to approve it (informational only;
|
|
203
|
+
the scheduler does not record approvers in the journal). ``now`` overrides the
|
|
204
|
+
generated-at timestamp, mainly for deterministic tests.
|
|
205
|
+
"""
|
|
206
|
+
ordered = sorted(events, key=lambda event: event.seq)
|
|
207
|
+
state: RunState = replay(list(ordered))
|
|
208
|
+
approver_map = approvers or {}
|
|
209
|
+
|
|
210
|
+
last_rule_done: dict[str, Event] = {}
|
|
211
|
+
last_rule_fail: dict[str, Event] = {}
|
|
212
|
+
last_gate_wait: dict[str, Event] = {}
|
|
213
|
+
last_run_stop: Event | None = None
|
|
214
|
+
for event in ordered:
|
|
215
|
+
if event.rule_instance is not None:
|
|
216
|
+
if event.type == "rule_done":
|
|
217
|
+
last_rule_done[event.rule_instance] = event
|
|
218
|
+
elif event.type == "rule_fail":
|
|
219
|
+
last_rule_fail[event.rule_instance] = event
|
|
220
|
+
elif event.type == "gate_wait":
|
|
221
|
+
last_gate_wait[event.rule_instance] = event
|
|
222
|
+
if event.type == "run_stop":
|
|
223
|
+
last_run_stop = event
|
|
224
|
+
|
|
225
|
+
done: list[str] = []
|
|
226
|
+
skipped_fresh: list[str] = []
|
|
227
|
+
failed: list[FailedItem] = []
|
|
228
|
+
waiting_gate: list[WaitingItem] = []
|
|
229
|
+
|
|
230
|
+
for iid in sorted(state.rules):
|
|
231
|
+
status = state.rules[iid]
|
|
232
|
+
if status == "done":
|
|
233
|
+
done_event = last_rule_done.get(iid)
|
|
234
|
+
if done_event is not None and done_event.payload.get("skipped") == "fresh":
|
|
235
|
+
skipped_fresh.append(iid)
|
|
236
|
+
else:
|
|
237
|
+
done.append(iid)
|
|
238
|
+
elif status == "failed":
|
|
239
|
+
fail_event = last_rule_fail.get(iid)
|
|
240
|
+
message = str(fail_event.payload.get("message", "")) if fail_event is not None else ""
|
|
241
|
+
agent = _agent_info(fail_event.payload) if fail_event is not None else None
|
|
242
|
+
failed.append(
|
|
243
|
+
FailedItem(
|
|
244
|
+
instance=iid, label=state.failures.get(iid), message=message, agent=agent
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
elif status == "waiting_gate":
|
|
248
|
+
wait_event = last_gate_wait.get(iid)
|
|
249
|
+
gate_id = str(wait_event.payload.get("gate", "")) if wait_event is not None else ""
|
|
250
|
+
waiting_gate.append(
|
|
251
|
+
WaitingItem(instance=iid, gate_id=gate_id, who=tuple(approver_map.get(gate_id, ())))
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
blocked_ids: list[str] = []
|
|
255
|
+
if last_run_stop is not None:
|
|
256
|
+
raw_blocked = last_run_stop.payload.get("blocked")
|
|
257
|
+
if isinstance(raw_blocked, list):
|
|
258
|
+
blocked_ids = [str(iid) for iid in raw_blocked]
|
|
259
|
+
blocked = tuple(sorted(set(blocked_ids)))
|
|
260
|
+
|
|
261
|
+
failed_t = tuple(failed)
|
|
262
|
+
waiting_gate_t = tuple(waiting_gate)
|
|
263
|
+
generated_at = now if now is not None else datetime.now(UTC)
|
|
264
|
+
|
|
265
|
+
return Handoff(
|
|
266
|
+
run_id=state.run_id,
|
|
267
|
+
target=target,
|
|
268
|
+
generated_at=generated_at,
|
|
269
|
+
done=tuple(done),
|
|
270
|
+
skipped_fresh=tuple(skipped_fresh),
|
|
271
|
+
failed=failed_t,
|
|
272
|
+
waiting_gate=waiting_gate_t,
|
|
273
|
+
blocked=blocked,
|
|
274
|
+
open_questions=_collect_open_questions(ordered),
|
|
275
|
+
next_steps=_next_steps(
|
|
276
|
+
target=target,
|
|
277
|
+
run_id=state.run_id,
|
|
278
|
+
failed=failed_t,
|
|
279
|
+
waiting_gate=waiting_gate_t,
|
|
280
|
+
blocked=blocked,
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def render_markdown(h: Handoff) -> str:
|
|
286
|
+
"""Render a short, deterministic ``HANDOFF.md`` body for a :class:`Handoff`."""
|
|
287
|
+
lines: list[str] = [
|
|
288
|
+
f"# Handoff — {h.target}",
|
|
289
|
+
f"Run `{h.run_id}` — generated {h.generated_at.isoformat()}",
|
|
290
|
+
"",
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
total_done = len(h.done) + len(h.skipped_fresh)
|
|
294
|
+
if total_done:
|
|
295
|
+
lines.append("## Done")
|
|
296
|
+
lines.append("")
|
|
297
|
+
if h.skipped_fresh:
|
|
298
|
+
lines.append(f"{total_done} done ({len(h.skipped_fresh)} already up to date, skipped).")
|
|
299
|
+
else:
|
|
300
|
+
lines.append(f"{total_done} done.")
|
|
301
|
+
if h.done:
|
|
302
|
+
lines.append("")
|
|
303
|
+
for instance in h.done:
|
|
304
|
+
lines.append(f"- {instance}")
|
|
305
|
+
lines.append("")
|
|
306
|
+
|
|
307
|
+
if h.waiting_gate:
|
|
308
|
+
lines.append("## Waiting for people")
|
|
309
|
+
lines.append("")
|
|
310
|
+
for wait_item in h.waiting_gate:
|
|
311
|
+
who = ", ".join(wait_item.who) if wait_item.who else "unknown"
|
|
312
|
+
lines.append(
|
|
313
|
+
f"- gate `{wait_item.gate_id}` for `{wait_item.instance}` — waiting on: {who}"
|
|
314
|
+
)
|
|
315
|
+
lines.append("")
|
|
316
|
+
|
|
317
|
+
if h.failed:
|
|
318
|
+
lines.append("## Failed")
|
|
319
|
+
lines.append("")
|
|
320
|
+
for fail_item in h.failed:
|
|
321
|
+
label = fail_item.label if fail_item.label is not None else "unlabeled"
|
|
322
|
+
lines.append(f"- `{fail_item.instance}` [{label}]: {fail_item.message}")
|
|
323
|
+
if fail_item.agent is not None:
|
|
324
|
+
lines.extend(_render_agent(fail_item.agent))
|
|
325
|
+
lines.append("")
|
|
326
|
+
|
|
327
|
+
if h.blocked:
|
|
328
|
+
lines.append("## Blocked")
|
|
329
|
+
lines.append("")
|
|
330
|
+
lines.append(f"{len(h.blocked)} blocked:")
|
|
331
|
+
for instance in h.blocked:
|
|
332
|
+
lines.append(f"- {instance}")
|
|
333
|
+
lines.append("")
|
|
334
|
+
|
|
335
|
+
if h.open_questions:
|
|
336
|
+
lines.append("## Open questions")
|
|
337
|
+
lines.append("")
|
|
338
|
+
for question in h.open_questions:
|
|
339
|
+
lines.append(f"- {question}")
|
|
340
|
+
lines.append("")
|
|
341
|
+
|
|
342
|
+
lines.append("## Next step")
|
|
343
|
+
lines.append("")
|
|
344
|
+
for i, step in enumerate(h.next_steps, start=1):
|
|
345
|
+
lines.append(f"{i}. {step}")
|
|
346
|
+
|
|
347
|
+
return "\n".join(lines) + "\n"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _render_agent(info: AgentStopInfo) -> list[str]:
|
|
351
|
+
"""The indented detail lines of an agent rule's stop, under its failed item."""
|
|
352
|
+
used = f"{info.tries} of {info.max_tries} tries"
|
|
353
|
+
if info.infra_failures:
|
|
354
|
+
used += f" (+{info.infra_failures} infra failures, not counted)"
|
|
355
|
+
spent = []
|
|
356
|
+
if info.tokens is not None:
|
|
357
|
+
spent.append(f"{info.tokens} tokens")
|
|
358
|
+
if info.cost is not None:
|
|
359
|
+
spent.append(f"${info.cost:.4f}")
|
|
360
|
+
lines = [f" - agent: {info.status}, reason `{info.reason}`, {used}"]
|
|
361
|
+
if spent:
|
|
362
|
+
lines[0] += "; spent " + ", ".join(spent)
|
|
363
|
+
if info.failures:
|
|
364
|
+
lines.append(f" - last failures [{info.label}]:")
|
|
365
|
+
lines.extend(f" - {failure}" for failure in info.failures)
|
|
366
|
+
elif info.failed_checks:
|
|
367
|
+
lines.append(f" - last failures [{info.label}]: {', '.join(info.failed_checks)}")
|
|
368
|
+
if info.stopped_earlier:
|
|
369
|
+
lines.append(" - stopped in an earlier run; not run again until an input changes")
|
|
370
|
+
return lines
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def render_status(state: RunState, *, target: str) -> str:
|
|
374
|
+
"""Render a compact plain-text status table for ``chipgraph status``."""
|
|
375
|
+
lines: list[str] = []
|
|
376
|
+
counts: dict[str, int] = {}
|
|
377
|
+
for iid in sorted(state.rules):
|
|
378
|
+
status = state.rules[iid]
|
|
379
|
+
counts[status] = counts.get(status, 0) + 1
|
|
380
|
+
lines.append(f"{status.upper():<12} {iid}")
|
|
381
|
+
|
|
382
|
+
total = len(state.rules)
|
|
383
|
+
parts = ", ".join(f"{count} {status}" for status, count in sorted(counts.items()))
|
|
384
|
+
summary = f"{total} rule(s) for {target}"
|
|
385
|
+
if parts:
|
|
386
|
+
summary += f": {parts}"
|
|
387
|
+
lines.append(summary)
|
|
388
|
+
|
|
389
|
+
return "\n".join(lines) + "\n"
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def write_handoff(layout: StateLayout, h: Handoff) -> Path:
|
|
393
|
+
"""Atomically write ``HANDOFF.md`` for a run's directory and return its path."""
|
|
394
|
+
path = layout.run_dir(h.run_id) / "HANDOFF.md"
|
|
395
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
396
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
397
|
+
tmp_path.write_text(render_markdown(h), encoding="utf-8")
|
|
398
|
+
os.replace(tmp_path, path)
|
|
399
|
+
return path
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
__all__ = [
|
|
403
|
+
"AgentStopInfo",
|
|
404
|
+
"FailedItem",
|
|
405
|
+
"Handoff",
|
|
406
|
+
"WaitingItem",
|
|
407
|
+
"build_handoff",
|
|
408
|
+
"render_markdown",
|
|
409
|
+
"render_status",
|
|
410
|
+
"write_handoff",
|
|
411
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Idempotency store: DESIGN.md 6.2, "Mỗi lần gọi tool có khóa idempotency, nên resume
|
|
2
|
+
không chạy lại sim hay synth đã xong" (every tool call has an idempotency key, so resume
|
|
3
|
+
does not re-run simulation or synthesis that already finished).
|
|
4
|
+
|
|
5
|
+
Results are stored as `CheckResult` JSON, one file per key, named by the key's SHA-256
|
|
6
|
+
hash, under the run's directory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from chipgraph.core.contracts.check import CheckResult
|
|
17
|
+
from chipgraph.core.state.layout import StateLayout
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make_key(parts: Mapping[str, str]) -> str:
|
|
21
|
+
"""A stable idempotency key: SHA-256 over `parts`, sorted by key, as ``k=v`` lines.
|
|
22
|
+
|
|
23
|
+
Callers pass whatever makes a tool call reproducible: the command, cwd, input hashes,
|
|
24
|
+
tool version, and so on.
|
|
25
|
+
"""
|
|
26
|
+
lines = "\n".join(f"{k}={parts[k]}" for k in sorted(parts))
|
|
27
|
+
return hashlib.sha256(lines.encode("utf-8")).hexdigest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IdempotencyStore:
|
|
31
|
+
"""Stores and retrieves `CheckResult`s by idempotency key, scoped to one run."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, layout: StateLayout, run_id: str) -> None:
|
|
34
|
+
self.layout = layout
|
|
35
|
+
self.run_id = run_id
|
|
36
|
+
self._dir = layout.run_dir(run_id) / "results"
|
|
37
|
+
|
|
38
|
+
def _path(self, key: str) -> Path:
|
|
39
|
+
digest = hashlib.sha256(key.encode("utf-8")).hexdigest()
|
|
40
|
+
return self._dir / f"{digest}.json"
|
|
41
|
+
|
|
42
|
+
def has(self, key: str) -> bool:
|
|
43
|
+
return self._path(key).exists()
|
|
44
|
+
|
|
45
|
+
def get(self, key: str) -> CheckResult | None:
|
|
46
|
+
path = self._path(key)
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return None
|
|
49
|
+
return CheckResult.model_validate_json(path.read_text(encoding="utf-8"))
|
|
50
|
+
|
|
51
|
+
def put(self, result: CheckResult) -> None:
|
|
52
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
path = self._path(result.idempotency_key)
|
|
54
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
55
|
+
tmp_path.write_text(result.model_dump_json(indent=2), encoding="utf-8")
|
|
56
|
+
os.replace(tmp_path, path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["IdempotencyStore", "make_key"]
|