sync: SHANGHAI-U9 2026-07-27
This commit is contained in:
@@ -30,6 +30,11 @@ def _run_command(command: Sequence[str], values: Mapping[str, str], timeout: int
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
# Windows with a CJK locale defaults text mode to GBK; the codex review
|
||||
# output is UTF-8 (Chinese) -> UnicodeDecodeError in the reader thread and
|
||||
# result.stdout comes back None. Pin UTF-8 and never hard-fail on decode.
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=timeout,
|
||||
)
|
||||
elapsed = time.monotonic() - t0
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
name: sim-rtl-flow
|
||||
description: >
|
||||
Run the sim-rtl-flow LangGraph workflow at E:\LangGraph_Design\sim-rtl-flow — the
|
||||
Simulink-to-RTL-to-hardware design pipeline: Simulink -x principle sim (sim-golden) →
|
||||
HUMAN sign-off on simulation performance → optional -q fixed-point → parallel
|
||||
functional partition + RTL/SoC interface spec → HUMAN sign-off on the partition →
|
||||
per-module -E.slx nativization and Xilinx-IP replacement (parallel Send fanout) →
|
||||
Vivado build + timing → hardware-in-the-loop via fpga-imx6-flow or zynq-imx6-flow.
|
||||
It drives the compiled graph in-process (no langgraph dev server) and hands every
|
||||
subagent task back to THIS Claude session — and every human checkpoint to the USER —
|
||||
through a record/replay ledger stored in the design project being worked on.
|
||||
|
||||
ALWAYS use this skill whenever the user wants to run, start, drive, resume or continue
|
||||
the sim-rtl-flow workflow / Simulink→RTL design flow, or says things like "启动
|
||||
sim-rtl-flow", "跑 sim-rtl-flow", "运行仿真到RTL的设计流程", "执行 sim-rtl-flow 设计任务",
|
||||
"继续 sim-rtl-flow", "run the sim to rtl flow", "resume the sim-rtl-flow run", even if
|
||||
they don't name the project path. Also use it when the user asks to record a subagent
|
||||
result, to approve or reject one of the two human checkpoints, or to rehearse the graph
|
||||
with the scripted backend.
|
||||
---
|
||||
|
||||
## What this skill does
|
||||
|
||||
Drives the **sim-rtl-flow** graph (21 top-level nodes + a 3-node per-module subgraph,
|
||||
1:1 with `Notes/PR01-1.md`, `Notes/PR01-2.md` and `Notes/PR01_sim_rtl_flow.drawio`).
|
||||
|
||||
The graph contains **no** agent logic — it talks to the outside world through two
|
||||
protocols: `SubagentBackend` (design work) and `ReviewGate` (the two mandatory human
|
||||
sign-offs). This skill plugs a **record/replay** implementation into both: the graph
|
||||
runs in-process until it needs an answer that isn't recorded yet, then stops and prints
|
||||
the request. **You are the subagent; the USER is the reviewer.** You do the work,
|
||||
`record` the answer, and `resume` — the driver replays the ledger, deterministically
|
||||
walks back to where it stopped, and asks for the next thing.
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Project root | 见 `~\skills_env.md` 的 `SIM_RTL_FLOW_ROOT`(driver 自动读取,也可 `--project` 覆盖) |
|
||||
| Conda env | `langgraph` (path 见 `~\skills_env.md` 的 `CONDA_LANGGRAPH_PYTHON`;装环境用 `/langgraph-vivado-install`) |
|
||||
| Graph | `sim_rtl_flow.graph:build_workflow` (in-process, no `langgraph dev`) |
|
||||
| Driver | `%USERPROFILE%\.claude\skills\sim-rtl-flow\scripts\run_sim_rtl_flow.py` |
|
||||
| **Run state** | **`<work_dir>\.sim_rtl_flow\runs\<thread-id>\`** — inside the *design project*, never in the workflow repo |
|
||||
| Max attempts | 10 per gate (default) |
|
||||
|
||||
There is **no LangGraph checkpointer** in the driver, on purpose: graph nodes have no
|
||||
side effects of their own (all side effects live in the subagents, which are replayed),
|
||||
so re-invoking from scratch is cheaper and immune to partial-superstep resume semantics.
|
||||
`ledger.json` *is* the persistence.
|
||||
|
||||
**Drive the driver with the Bash tool (Git Bash), not PowerShell** — PowerShell 5.1
|
||||
mangles the double quotes inside `--result '{...}'` payloads. Every command below was
|
||||
run this way and verified.
|
||||
|
||||
```bash
|
||||
PY="<CONDA_LANGGRAPH_PYTHON — see ~\skills_env.md>"
|
||||
RUN="$USERPROFILE/.claude/skills/sim-rtl-flow/scripts/run_sim_rtl_flow.py"
|
||||
export PYTHONIOENCODING=utf-8
|
||||
```
|
||||
|
||||
The driver resolves the workflow checkout from `SIM_RTL_FLOW_ROOT` in
|
||||
`~\skills_env.md`, so no path is baked into this skill.
|
||||
|
||||
## ⚠️ The two human checkpoints are the user's decision, not yours
|
||||
|
||||
The graph stops at two points that **an agent may not decide**:
|
||||
|
||||
| checkpoint | where | what the user confirms |
|
||||
|---|---|---|
|
||||
| `sim_performance` | after step 1 | the `-x` sim meets the requirement and may be frozen as **sim-golden** |
|
||||
| `functional_partition` | after step 3 | the pure-sim / RTL / SoC split and the RTL↔SoC interface doc |
|
||||
|
||||
When one comes up the driver prints a `!!!` banner. **Relay the question and its context
|
||||
to the user verbatim, then wait.** Do not approve on your own judgement, do not infer
|
||||
approval from "the numbers look fine", and do not use `--all-ok` (the driver refuses it
|
||||
for review keys anyway). A rejection **must** carry the user's reason.
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" --work-dir <proj> --thread-id <id> record --key review:sim_performance#1 --approve
|
||||
"$PY" "$RUN" --work-dir <proj> --thread-id <id> record --key review:sim_performance#1 \
|
||||
--reject --notes "带外抑制不足 6dB"
|
||||
```
|
||||
|
||||
Rejecting `sim_performance` re-runs step 1; rejecting `functional_partition` re-runs the
|
||||
whole partition + interface superstep.
|
||||
|
||||
## Non-negotiable design rules the graph enforces
|
||||
|
||||
From `Notes/PR01.md` and `Notes/PR01-2.md`; the graph routes on these.
|
||||
|
||||
1. **sim-golden is the only acceptance criterion.** `-q`, the partition, and every `-E`
|
||||
version must match the `-x` sim. If it doesn't, record `gate_passed: false` — never
|
||||
record a pass you didn't verify.
|
||||
2. **One `-E.slx` per RTL submodule**, saved separately (`<project>_A-E.slx`, …). Do not
|
||||
nativize all submodules in one Simulink model.
|
||||
3. **Never hand-edit MATLAB-generated RTL.** If IP replacement, Vivado timing, or HIL
|
||||
needs a change there, return `change_scope: "simulink_module"` + `affected_modules`.
|
||||
The graph then forces a `modify_e:*` step — **edit the `-E.slx` and re-simulate
|
||||
against sim-golden** — before any RTL is regenerated.
|
||||
4. **Timing fixes change the RTL, not the constraints.** Prefer adding pipeline/delay
|
||||
stages. Only touch timing constraints when the constraint itself is plainly wrong,
|
||||
and say why in `notes`.
|
||||
5. **HIL is strictly read-only.** `hil:*` arrives with `allow_code_changes=false`. Run
|
||||
`fpga-imx6-flow` / `zynq-imx6-flow` for *diagnosis only*; the moment a code change is
|
||||
needed, stop that sub-flow and report it back here — the fix happens in the
|
||||
sim-rtl-flow main graph, then re-enters Vivado and HIL.
|
||||
|
||||
## Run (agent path)
|
||||
|
||||
### 1. Write the workflow input
|
||||
|
||||
`work_dir` is the design project. **All run state goes under
|
||||
`<work_dir>/.sim_rtl_flow/`** — that is where `resume`/`record`/`status` will look.
|
||||
|
||||
```bash
|
||||
cat > /c/Users/Administrator/AppData/Local/Temp/claude/input.json <<'EOF'
|
||||
{
|
||||
"requirements_path": "Notes/requirements.md",
|
||||
"work_dir": "E:/Xilinx_design/my_project",
|
||||
"project_name": "demo",
|
||||
"device_family": "fpga-imx6",
|
||||
"fixed_point_required": true,
|
||||
"rtl_modules": ["A", "B", "C"],
|
||||
"soc_modules": ["control"]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
| field | meaning |
|
||||
|---|---|
|
||||
| `requirements_path` | user's requirement `.md`, relative to `work_dir` |
|
||||
| `work_dir` | the design project — **absolute path**; also where run state is written |
|
||||
| `project_name` | artifact prefix — `<name>-x.slx`, `<name>_A-E.slx`, `<name>.bit` |
|
||||
| `device_family` | `fpga-imx6` → HIL via `fpga-imx6-flow`; `zynq-imx6` → `zynq-imx6-flow`. Anything else raises at `initialize`. |
|
||||
| `fixed_point_required` | `false` skips the `-q` stage |
|
||||
| `rtl_modules` | submodule names; each gets its own parallel `native → ip_replace` chain. Non-empty. |
|
||||
| `soc_modules` | MCU/SoC-side modules (interface-spec input) |
|
||||
|
||||
### 2. Start — exits 10 with a handoff
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" --thread-id my-design-001 start --input /c/Users/Administrator/AppData/Local/Temp/claude/input.json
|
||||
```
|
||||
|
||||
`start` takes `work_dir` from the input JSON and prints where state landed. Verified:
|
||||
|
||||
```
|
||||
work dir : E:\Xilinx_design\my_project
|
||||
run dir : E:\Xilinx_design\my_project\.sim_rtl_flow\runs\my-design-001
|
||||
========================================================================
|
||||
HANDOFF — 1 subagent task(s), 0 human review(s)
|
||||
========================================================================
|
||||
--- key: simulink_x#1
|
||||
role : simulink-principle-agent
|
||||
permission : WRITE ALLOWED
|
||||
prompt : 根据需求文档创建或修订 Simulink -x 原理仿真;...
|
||||
```
|
||||
|
||||
Exit codes: **10** = handoff pending, **0** = completed, **1** = `failed`, **2** = crash.
|
||||
|
||||
### 3. Answer, then resume
|
||||
|
||||
Every later command needs to find the run, so pass `--work-dir` (or `cd` into it):
|
||||
|
||||
```bash
|
||||
W="--work-dir E:/Xilinx_design/my_project --thread-id my-design-001"
|
||||
"$PY" "$RUN" $W record --key simulink_x#1 \
|
||||
--result '{"gate_passed": true, "artifacts": ["demo-x.slx"], "notes": "EVM -38dB"}'
|
||||
"$PY" "$RUN" $W resume
|
||||
```
|
||||
|
||||
Loop until `WORKFLOW FINISHED — status: completed`. When several keys are pending,
|
||||
record **each**, then `resume` once. `record --all-ok` fast-forwards pending *subagent*
|
||||
keys with a default success — rehearsal only, and it refuses review keys.
|
||||
|
||||
### 4. What you get handed
|
||||
|
||||
| key | what you actually do | which skill to use |
|
||||
|---|---|---|
|
||||
| `simulink_x#n` | build/revise the `-x` sim, document the performance envelope | MATLAB MCP + `model-based-design-core:building-simulink-models`, `simulating-simulink-models` |
|
||||
| `review:sim_performance#n` | **ask the user** to sign off on sim-golden | — (relay to user) |
|
||||
| `fixed_point#n` | `-q` fixed-point, compare to `-x` | same MATLAB skills |
|
||||
| `partition#n` | pure-sim / RTL / SoC split, must not degrade sim-golden | same |
|
||||
| `interfaces#n` | RTL↔SoC interface doc — register-config or data-stream only | write the doc under `work_dir` |
|
||||
| `review:functional_partition#n` | **ask the user** to sign off on the split + interfaces | — (relay to user) |
|
||||
| `module_plan#n` | split the RTL side into A/B/C…, give order + dependencies (**first pass only**) | inline |
|
||||
| `native:<M>#n` | nativize M into HDL-capable blocks, save `<project>_M-E.slx`, generate RTL | request says `simulink-rtl-dev` — **not installed**; use MATLAB MCP + model-based-design-core |
|
||||
| `ip_replace:<M>#n` | swap in Xilinx IP, cycle-true compare vs MATLAB RTL | request says `rtl-ip-replace` — **not installed**; use `vivado-sim` + `vivado-mcp` |
|
||||
| `modify_e:vivado#n` / `modify_e:hil#n` | **edit the named `-E.slx` and re-simulate** against sim-golden | MATLAB MCP + model-based-design-core |
|
||||
| `vivado#n` | build the Vivado project, synth+impl+bitstream, check all timing | `vivado-prj-create`, `vivado-mcp` |
|
||||
| `hil:fpga-imx6-flow#n` / `hil:zynq-imx6-flow#n` | HIL debug, **read-only** | `fpga-imx6-flow` / `zynq-imx6-flow` |
|
||||
| `timing_rework#n` | fix non-Simulink RTL / SoC / project timing | inline |
|
||||
| `hil_rework#n` | apply the code change HIL asked for | inline |
|
||||
|
||||
`#n` is the n-th time that key has been requested this run — retries and rework produce
|
||||
`#2`, `#3`, …
|
||||
|
||||
### 5. Answer JSON
|
||||
|
||||
`"$PY" "$RUN" schema` prints both templates. All fields optional except `approved`.
|
||||
|
||||
```json
|
||||
subagent: {"success": true, "gate_passed": true, "artifacts": [], "notes": "",
|
||||
"timing_met": true, "hil_passed": true, "needs_code_change": false,
|
||||
"change_scope": "none", "affected_modules": [], "metadata": {}}
|
||||
review: {"approved": true, "notes": "", "metadata": {}}
|
||||
```
|
||||
|
||||
| field | who reads it |
|
||||
|---|---|
|
||||
| `success` + `gate_passed` | every design node — `false` → retry, then `failed` after 10 attempts |
|
||||
| `artifacts` | `simulink_x`'s first artifact **becomes sim-golden**; `vivado`'s go to HIL as the bitstream |
|
||||
| `timing_met` | `vivado` only |
|
||||
| `hil_passed` | `hil:*` only |
|
||||
| `needs_code_change` + `change_scope` + `affected_modules` | `vivado` and `hil:*` — the rework router |
|
||||
|
||||
Routing on `change_scope`:
|
||||
|
||||
- `simulink_module` → **`modify_e:*`** (edit `-E.slx` + re-simulate) → only the affected
|
||||
modules re-run `native → ip_replace` → Vivado → HIL.
|
||||
- `non_simulink_rtl` / `soc` / `hardware` → `timing_rework` (from Vivado) or `hil_rework`
|
||||
(from HIL) → Vivado.
|
||||
- The driver **rejects** `needs_code_change: true` with `change_scope: "none"`.
|
||||
|
||||
### 6. Parallel batches (verified)
|
||||
|
||||
| stage | keys handed over at once |
|
||||
|---|---|
|
||||
| after `-x` / `-q` | `interfaces#1` **and** `partition#1` |
|
||||
| module fanout | one `native:<M>#1` per module (3 for `["A","B","C"]`) |
|
||||
| next module stage | one `ip_replace:<M>#1` per module |
|
||||
|
||||
Work batched tasks in any order; record all, then `resume`.
|
||||
|
||||
## Rehearsal without touching hardware
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" dry-run --input examples/happy_path.json
|
||||
```
|
||||
|
||||
Runs the whole graph against `ScriptedBackend` + auto-approving `ScriptedReviewGate` —
|
||||
no handoffs, no side effects. Verified: `status: completed`, 13 subagent calls listed
|
||||
with their skill and write permission, plus both review checkpoints. To rehearse a
|
||||
branch, add `outcomes` (task → list of results) and/or `reviews` (checkpoint → list of
|
||||
decisions) to the input JSON; `start`/`resume` strip both.
|
||||
|
||||
```json
|
||||
"outcomes": {"hil:fpga-imx6-flow": [
|
||||
{"success": true, "hil_passed": false, "needs_code_change": true,
|
||||
"change_scope": "simulink_module", "affected_modules": ["A"]},
|
||||
{"success": true, "hil_passed": true}]},
|
||||
"reviews": {"sim_performance": [{"approved": false, "notes": "EVM 未达标"}]}
|
||||
```
|
||||
|
||||
## Other commands
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" $W status # ledger + pending + final status
|
||||
"$PY" "$RUN" schema # both answer templates
|
||||
"$PY" "$RUN" mermaid # graph as Mermaid (see gotcha)
|
||||
"$PY" -m unittest discover -s tests # project's own 16 tests — all pass
|
||||
```
|
||||
|
||||
Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread-id`,
|
||||
`--recursion-limit` (200), `--max-attempts` (10).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`resume`/`record`/`status` need `--work-dir`** (or a `cd` into the design project).
|
||||
Only `start` can infer it, from the input JSON. Without it the driver looks in the cwd
|
||||
and reports `no input.json in …`.
|
||||
- **The driver refuses a `work_dir` inside the sim-rtl-flow repo.** Run state belongs to
|
||||
the design project (PR01-2). `examples/happy_path.json` therefore carries a placeholder
|
||||
`E:/Xilinx_design/your_project` — fine for `dry-run`, but replace it before `start`.
|
||||
- **`mermaid` output is still partly misleading.** The forward and rework edges now
|
||||
render (path maps were added), but `plan_modules`' `Send` fanout is a dynamic edge
|
||||
Mermaid can't show. Use `Notes/Result00.md` for the authoritative picture.
|
||||
- **`--max-attempts` must precede the subcommand** and stay the *same value* across
|
||||
`start` and every `resume` — it is runtime context, not persisted state.
|
||||
- **The two skills the graph names don't exist.** `native:*` suggests `simulink-rtl-dev`,
|
||||
`ip_replace:*` suggests `rtl-ip-replace`; neither is installed. Fall back to the MATLAB
|
||||
MCP + `model-based-design-core:*` skills and `vivado-sim` / `vivado-mcp`. Don't abort.
|
||||
- **`simulink_x`'s first artifact silently becomes sim-golden.** Record `artifacts: []`
|
||||
and the graph substitutes `<project_name>-x.slx`. Always record the real filename.
|
||||
- **`module_plan` runs only on the first pass.** Rework rounds reuse the plan, so you
|
||||
will not be asked to re-partition the module list mid-run.
|
||||
- **Re-recording a key requires editing `ledger.json` by hand.** `record` only accepts
|
||||
currently-pending keys; anything already in the ledger is replayed forever. To redo a
|
||||
step, delete its entry *and everything after it*, then `resume`.
|
||||
- **Runs created before the PR01-2 change live in the old location**
|
||||
(`<SIM_RTL_FLOW_ROOT>\runs\<id>\`) and their ledgers have no `review:*` or
|
||||
`module_plan` keys, so they cannot be resumed under the current graph. Start a fresh
|
||||
thread instead.
|
||||
- **`work_dir` is passed straight through to every subagent request.** Forward slashes
|
||||
are fine on Windows; a relative `"."` resolves against wherever the driver was
|
||||
launched — always use an absolute path.
|
||||
- **`start` wipes the ledger** unless `--keep-ledger`. Use a fresh `--thread-id` per run.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| symptom | cause / fix |
|
||||
|---|---|
|
||||
| `sim_rtl_flow package not found under …\src` | `SIM_RTL_FLOW_ROOT` in `~\skills_env.md` is wrong/missing; fix it or pass `--project <检出目录>` |
|
||||
| `sim-rtl-flow 工作流仓库路径未配置` | add `SIM_RTL_FLOW_ROOT = <检出目录>` to `~\skills_env.md` |
|
||||
| `no input.json in …\.sim_rtl_flow\runs\<id>` | missing/incorrect `--work-dir`, or `start` was never run |
|
||||
| `nothing pending — run 'start' or 'resume' first` | the previous invoke completed or crashed; check `status` |
|
||||
| `--key X is not pending; pending = [...]` | typo, or already recorded (see the re-record gotcha) |
|
||||
| `--all-ok 不能用于人工审核断点` | working as intended — the user must decide; use `--approve` / `--reject` |
|
||||
| `驳回必须写明理由:请提供 notes` | add `--notes "<user's reason>"` |
|
||||
| `unknown review fields: ['gate_passed']` | you sent a subagent result to a `review:*` key; use `--approve`/`--reject` |
|
||||
| `needs_code_change=true requires an explicit change_scope` | add `change_scope` |
|
||||
| `MissingReviewGateError` | only if the graph is driven without a review gate — the driver always supplies one |
|
||||
| `device_family must be 'fpga-imx6' or 'zynq-imx6'` | raised in `initialize`; exit 2 |
|
||||
| exit 2 + `recursion_limit` | a rework loop isn't converging — raise `--recursion-limit`, or stop recording `needs_code_change` for the same module |
|
||||
| mojibake in the Chinese prompts | `export PYTHONIOENCODING=utf-8` |
|
||||
@@ -0,0 +1,680 @@
|
||||
"""Drive the sim-rtl-flow LangGraph workflow from a Claude Code session.
|
||||
|
||||
The graph never talks to an agent host directly: subagent work goes through the
|
||||
``SubagentBackend`` protocol and the two mandatory human checkpoints go through
|
||||
the ``ReviewGate`` protocol. This driver plugs a *record/replay* implementation
|
||||
into both, so the agent host can be **this Claude session** and the reviewer can
|
||||
be **the user**:
|
||||
|
||||
* when the graph asks for something that has no recorded answer yet, the driver
|
||||
stops the run, writes the pending request(s) to ``pending.json`` and prints
|
||||
them;
|
||||
* Claude performs the subagent task (or relays the review request to the user)
|
||||
and calls ``record``;
|
||||
* ``resume`` re-invokes the graph from scratch -- every already-recorded call is
|
||||
replayed from the ledger, so the graph deterministically walks back to where
|
||||
it stopped and asks for the next thing.
|
||||
|
||||
Replay-from-scratch is why no LangGraph checkpointer is used here: graph nodes
|
||||
have no side effects of their own (all side effects live in the subagents, which
|
||||
are replayed from the ledger), so a fresh invoke is both cheaper and immune to
|
||||
partial-superstep resume semantics.
|
||||
|
||||
All run state lives under ``<work_dir>/.sim_rtl_flow/`` -- i.e. inside the
|
||||
project being designed, never inside the sim-rtl-flow repository (Notes/PR01-2.md).
|
||||
|
||||
Commands
|
||||
--------
|
||||
start --input <json> [--thread-id ID] begin a run
|
||||
resume continue after record
|
||||
record --key K --result '<json>' record one subagent result
|
||||
record --key K --approve | --reject answer a human review checkpoint
|
||||
record --all-ok default-success for pending SUBAGENTS
|
||||
status show ledger / pending / last state
|
||||
dry-run --input <json> end-to-end ScriptedBackend rehearsal
|
||||
mermaid print the graph as Mermaid
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _env_lookup(key: str, default: str | None = None) -> str | None:
|
||||
"""Read KEY from the OS environment, else from a ``KEY = value`` line in
|
||||
~/skills_env.md. Machine-local paths are never hardcoded in a skill; see
|
||||
the same helper in fpga_flash/scripts/vivado_env.py."""
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
return value
|
||||
path = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
match = re.match(rf"\s*{re.escape(key)}\s*=\s*(.+?)\s*(?:#.*)?$", line)
|
||||
if match:
|
||||
found = match.group(1).strip()
|
||||
if found and not found.startswith("<"):
|
||||
return found
|
||||
except OSError:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
def _default_project() -> Path:
|
||||
"""The sim-rtl-flow checkout: SIM_RTL_FLOW_ROOT in ~/skills_env.md."""
|
||||
root = _env_lookup("SIM_RTL_FLOW_ROOT")
|
||||
if root:
|
||||
return Path(root)
|
||||
raise SystemExit(
|
||||
"sim-rtl-flow 工作流仓库路径未配置:请在 ~/skills_env.md 增加\n"
|
||||
" SIM_RTL_FLOW_ROOT = <sim-rtl-flow 检出目录>\n"
|
||||
"或用 --project 显式指定。"
|
||||
)
|
||||
|
||||
#: Run state lives inside the *calling* project, not in the workflow repo.
|
||||
STATE_DIRNAME = ".sim_rtl_flow"
|
||||
|
||||
CHANGE_SCOPES = {
|
||||
"none",
|
||||
"simulink_module",
|
||||
"non_simulink_rtl",
|
||||
"soc",
|
||||
"hardware",
|
||||
"unknown",
|
||||
}
|
||||
|
||||
RESULT_DEFAULTS: dict[str, Any] = {
|
||||
"success": True,
|
||||
"gate_passed": True,
|
||||
"artifacts": [],
|
||||
"notes": "",
|
||||
"timing_met": True,
|
||||
"hil_passed": True,
|
||||
"needs_code_change": False,
|
||||
"change_scope": "none",
|
||||
"affected_modules": [],
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
REVIEW_DEFAULTS: dict[str, Any] = {"approved": True, "notes": "", "metadata": {}}
|
||||
|
||||
|
||||
def _bootstrap(project: Path):
|
||||
src = project / "src"
|
||||
if not (src / "sim_rtl_flow").is_dir():
|
||||
raise SystemExit(f"sim_rtl_flow package not found under {src}")
|
||||
sys.path.insert(0, str(src))
|
||||
from sim_rtl_flow.backend import ( # noqa: E402
|
||||
ScriptedBackend,
|
||||
SubagentResult,
|
||||
)
|
||||
from sim_rtl_flow.graph import WorkflowContext, build_workflow # noqa: E402
|
||||
from sim_rtl_flow.review import ReviewDecision, ScriptedReviewGate # noqa: E402
|
||||
|
||||
return {
|
||||
"ScriptedBackend": ScriptedBackend,
|
||||
"ScriptedReviewGate": ScriptedReviewGate,
|
||||
"SubagentResult": SubagentResult,
|
||||
"ReviewDecision": ReviewDecision,
|
||||
"WorkflowContext": WorkflowContext,
|
||||
"build_workflow": build_workflow,
|
||||
}
|
||||
|
||||
|
||||
class HandoffRequired(Exception):
|
||||
"""Raised inside a graph node when an answer is not recorded yet."""
|
||||
|
||||
def __init__(self, key: str) -> None:
|
||||
super().__init__(f"handoff required: {key}")
|
||||
self.key = key
|
||||
|
||||
|
||||
class Handoffs:
|
||||
"""Shared record/replay ledger for subagent tasks and human reviews."""
|
||||
|
||||
def __init__(self, ledger: dict[str, Any]) -> None:
|
||||
self._ledger = ledger
|
||||
self._counter: dict[str, int] = defaultdict(int)
|
||||
self._lock = threading.Lock()
|
||||
self.pending: list[dict[str, Any]] = []
|
||||
self.replayed: list[str] = []
|
||||
|
||||
def resolve(self, name: str, describe) -> dict[str, Any]:
|
||||
"""Return the recorded payload for ``name``'s next occurrence, or halt."""
|
||||
with self._lock:
|
||||
self._counter[name] += 1
|
||||
key = f"{name}#{self._counter[name]}"
|
||||
recorded = self._ledger.get(key)
|
||||
if recorded is not None:
|
||||
self.replayed.append(key)
|
||||
return recorded
|
||||
self.pending.append({"key": key, **describe()})
|
||||
raise HandoffRequired(key)
|
||||
|
||||
|
||||
class ClaudeSessionBackend:
|
||||
"""Replay recorded subagent results; hand unknown requests to Claude."""
|
||||
|
||||
def __init__(self, handoffs: Handoffs, SubagentResult) -> None:
|
||||
self._handoffs = handoffs
|
||||
self._result_cls = SubagentResult
|
||||
|
||||
def run(self, request): # noqa: ANN001 - duck-typed SubagentRequest
|
||||
payload = self._handoffs.resolve(
|
||||
request.task,
|
||||
lambda: {
|
||||
"kind": "subagent",
|
||||
"task": request.task,
|
||||
"role": request.role,
|
||||
"skill": request.skill,
|
||||
"module": request.module,
|
||||
"allow_code_changes": request.allow_code_changes,
|
||||
"work_dir": request.work_dir,
|
||||
"prompt": request.prompt,
|
||||
"context": request.context,
|
||||
},
|
||||
)
|
||||
values = {**RESULT_DEFAULTS, **payload}
|
||||
values["artifacts"] = tuple(values["artifacts"])
|
||||
values["affected_modules"] = tuple(values["affected_modules"])
|
||||
return self._result_cls(**values)
|
||||
|
||||
|
||||
class ClaudeSessionReviewGate:
|
||||
"""Relay the two mandatory human checkpoints to the user via Claude."""
|
||||
|
||||
def __init__(self, handoffs: Handoffs, ReviewDecision) -> None:
|
||||
self._handoffs = handoffs
|
||||
self._decision_cls = ReviewDecision
|
||||
|
||||
def review(self, request): # noqa: ANN001 - duck-typed ReviewRequest
|
||||
payload = self._handoffs.resolve(
|
||||
f"review:{request.checkpoint}",
|
||||
lambda: {
|
||||
"kind": "review",
|
||||
"checkpoint": request.checkpoint,
|
||||
"title": request.title,
|
||||
"work_dir": request.work_dir,
|
||||
"prompt": request.prompt,
|
||||
"context": request.context,
|
||||
},
|
||||
)
|
||||
values = {**REVIEW_DEFAULTS, **payload}
|
||||
return self._decision_cls(**values)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# run directory helpers
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RunDir:
|
||||
"""``<work_dir>/.sim_rtl_flow/runs/<thread-id>/`` -- inside the design project."""
|
||||
|
||||
def __init__(self, work_dir: Path, thread_id: str, *, create: bool = True) -> None:
|
||||
self.work_dir = work_dir.expanduser().resolve()
|
||||
self.thread_id = thread_id
|
||||
self.path = self.work_dir / STATE_DIRNAME / "runs" / thread_id
|
||||
if create:
|
||||
self.path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _read(self, name: str, default: Any) -> Any:
|
||||
target = self.path / name
|
||||
if not target.exists():
|
||||
return default
|
||||
return json.loads(target.read_text(encoding="utf-8"))
|
||||
|
||||
def _write(self, name: str, payload: Any) -> None:
|
||||
self.path.mkdir(parents=True, exist_ok=True)
|
||||
(self.path / name).write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
|
||||
@property
|
||||
def workflow_input(self) -> dict[str, Any]:
|
||||
data = self._read("input.json", None)
|
||||
if data is None:
|
||||
raise SystemExit(
|
||||
f"no input.json in {self.path}\n"
|
||||
"run `start --input <file>` first, or point --work-dir at the "
|
||||
"design project whose .sim_rtl_flow/ holds this run"
|
||||
)
|
||||
return data
|
||||
|
||||
def set_input(self, payload: dict[str, Any]) -> None:
|
||||
self._write("input.json", payload)
|
||||
|
||||
@property
|
||||
def ledger(self) -> dict[str, Any]:
|
||||
return self._read("ledger.json", {})
|
||||
|
||||
def set_ledger(self, payload: dict[str, Any]) -> None:
|
||||
self._write("ledger.json", payload)
|
||||
|
||||
@property
|
||||
def pending(self) -> list[dict[str, Any]]:
|
||||
return self._read("pending.json", [])
|
||||
|
||||
def set_pending(self, payload: list[dict[str, Any]]) -> None:
|
||||
self._write("pending.json", payload)
|
||||
|
||||
def set_state(self, payload: Any) -> None:
|
||||
self._write("last_state.json", payload)
|
||||
|
||||
@property
|
||||
def state(self) -> Any:
|
||||
return self._read("last_state.json", None)
|
||||
|
||||
|
||||
def _resolve_work_dir(args, payload: dict[str, Any] | None = None) -> Path:
|
||||
"""The calling project's directory -- where all run state must live."""
|
||||
if payload is not None:
|
||||
work_dir = payload.get("work_dir")
|
||||
if not work_dir:
|
||||
raise SystemExit("workflow input must define work_dir")
|
||||
resolved = Path(work_dir)
|
||||
elif args.work_dir is not None:
|
||||
resolved = Path(args.work_dir)
|
||||
else:
|
||||
resolved = Path.cwd()
|
||||
|
||||
resolved = resolved.expanduser().resolve()
|
||||
project = Path(args.project).expanduser().resolve()
|
||||
if resolved == project or project in resolved.parents:
|
||||
raise SystemExit(
|
||||
f"work_dir 不能位于 sim-rtl-flow 工作流仓库内({resolved})。\n"
|
||||
"运行状态必须保存在调用这个工作流的那个工程目录下 —— "
|
||||
"请把 work_dir 指向真实的设计工程。"
|
||||
)
|
||||
return resolved
|
||||
|
||||
|
||||
def _jsonable(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {str(k): _jsonable(v) for k, v in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_jsonable(v) for v in value]
|
||||
if isinstance(value, (str, int, float, bool)) or value is None:
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
|
||||
def _unwrap(exc: BaseException) -> HandoffRequired | None:
|
||||
seen: set[int] = set()
|
||||
stack: list[BaseException] = [exc]
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if current is None or id(current) in seen:
|
||||
continue
|
||||
seen.add(id(current))
|
||||
if isinstance(current, HandoffRequired):
|
||||
return current
|
||||
if isinstance(current, BaseExceptionGroup): # noqa: F821 - py3.11+
|
||||
stack.extend(current.exceptions)
|
||||
stack.append(current.__cause__)
|
||||
stack.append(current.__context__)
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# printing
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _print_pending(pending: list[dict[str, Any]]) -> None:
|
||||
reviews = [item for item in pending if item.get("kind") == "review"]
|
||||
tasks = [item for item in pending if item.get("kind") != "review"]
|
||||
|
||||
print("\n" + "=" * 72)
|
||||
print(
|
||||
f"HANDOFF — {len(tasks)} subagent task(s), {len(reviews)} human review(s)"
|
||||
)
|
||||
print("=" * 72)
|
||||
|
||||
for item in reviews:
|
||||
print("\n" + "!" * 72)
|
||||
print(f"!! 人工审核确认 — key: {item['key']}")
|
||||
print(f"!! {item['title']}")
|
||||
print("!! 必须把该问题原样转达用户并等待用户决定;")
|
||||
print("!! Claude 不得代替用户批准,也不得依据自己的判断放行。")
|
||||
print("!" * 72)
|
||||
print(f" checkpoint : {item['checkpoint']}")
|
||||
print(f" work_dir : {item['work_dir']}")
|
||||
print(f" 问题 : {item['prompt']}")
|
||||
print(" context : " + json.dumps(item["context"], ensure_ascii=False))
|
||||
|
||||
for item in tasks:
|
||||
print(f"\n--- key: {item['key']}")
|
||||
print(f" role : {item['role']}")
|
||||
print(f" skill : {item['skill'] or '(none — do it inline)'}")
|
||||
print(f" module : {item['module'] or '-'}")
|
||||
print(f" work_dir : {item['work_dir']}")
|
||||
write = (
|
||||
"WRITE ALLOWED"
|
||||
if item["allow_code_changes"]
|
||||
else "READ-ONLY (no code edits!)"
|
||||
)
|
||||
print(f" permission : {write}")
|
||||
print(f" prompt : {item['prompt']}")
|
||||
print(" context : " + json.dumps(item["context"], ensure_ascii=False))
|
||||
|
||||
print("\n" + "-" * 72)
|
||||
print("Answer each, then resume:")
|
||||
for item in reviews:
|
||||
print(f" record --key {item['key']} --approve # 用户批准后")
|
||||
print(f" record --key {item['key']} --reject --notes '…' # 用户驳回后")
|
||||
for item in tasks:
|
||||
print(
|
||||
f" record --key {item['key']} "
|
||||
"--result '{\"gate_passed\": true, \"notes\": \"...\"}'"
|
||||
)
|
||||
print(" resume")
|
||||
print("-" * 72)
|
||||
|
||||
|
||||
def _print_final(state: Any, replayed: list[str]) -> None:
|
||||
status = state.get("status") if isinstance(state, dict) else None
|
||||
print("\n" + "=" * 72)
|
||||
print(f"WORKFLOW FINISHED — status: {status}")
|
||||
print("=" * 72)
|
||||
if isinstance(state, dict):
|
||||
if state.get("failure_reason"):
|
||||
print(f"failure_reason : {state['failure_reason']}")
|
||||
for label, key in (("人工确认 · 仿真性能", "sim_review"),
|
||||
("人工确认 · 功能划分", "partition_review")):
|
||||
review = state.get(key)
|
||||
if review:
|
||||
mark = "批准" if review.get("approved") else "驳回"
|
||||
print(f"{label} : {mark} {review.get('notes', '')}".rstrip())
|
||||
print(f"modules : {state.get('rtl_modules')}")
|
||||
for item in state.get("module_results", []):
|
||||
print(
|
||||
f" - {item['module']}: stage={item['stage']} "
|
||||
f"success={item['success']} artifacts={item.get('artifacts')}"
|
||||
)
|
||||
print(f"vivado : {_jsonable(state.get('vivado_result'))}")
|
||||
print(f"hil : {_jsonable(state.get('hil_result'))}")
|
||||
print(f"\nevents ({len(state.get('events', []))}):")
|
||||
for event in state.get("events", []):
|
||||
print(" " + json.dumps(_jsonable(event), ensure_ascii=False))
|
||||
print(f"\nreplayed answers: {len(replayed)}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# core invoke
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _invoke(run: RunDir, api: dict[str, Any], args) -> int:
|
||||
handoffs = Handoffs(run.ledger)
|
||||
graph = api["build_workflow"]()
|
||||
context = api["WorkflowContext"](
|
||||
backend=ClaudeSessionBackend(handoffs, api["SubagentResult"]),
|
||||
max_attempts=args.max_attempts,
|
||||
review_gate=ClaudeSessionReviewGate(handoffs, api["ReviewDecision"]),
|
||||
)
|
||||
try:
|
||||
state = graph.invoke(
|
||||
run.workflow_input,
|
||||
{"recursion_limit": args.recursion_limit},
|
||||
context=context,
|
||||
)
|
||||
except BaseException as exc: # noqa: BLE001 - re-raised unless it is a handoff
|
||||
handoff = _unwrap(exc)
|
||||
if handoff is None:
|
||||
traceback.print_exc()
|
||||
return 2
|
||||
run.set_pending(handoffs.pending)
|
||||
_print_pending(handoffs.pending)
|
||||
return 10
|
||||
|
||||
run.set_pending([])
|
||||
run.set_state(_jsonable(state))
|
||||
_print_final(state, handoffs.replayed)
|
||||
return 0 if state.get("status") == "completed" else 1
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# commands
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def cmd_start(args, api) -> int:
|
||||
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
||||
payload.pop("outcomes", None)
|
||||
run = RunDir(_resolve_work_dir(args, payload), args.thread_id)
|
||||
run.set_input(payload)
|
||||
if not args.keep_ledger:
|
||||
run.set_ledger({})
|
||||
run.set_pending([])
|
||||
print(f"work dir : {run.work_dir}")
|
||||
print(f"run dir : {run.path}")
|
||||
print(f"thread : {run.thread_id}")
|
||||
print("input : " + json.dumps(payload, ensure_ascii=False))
|
||||
return _invoke(run, api, args)
|
||||
|
||||
|
||||
def cmd_resume(args, api) -> int:
|
||||
run = RunDir(_resolve_work_dir(args), args.thread_id, create=False)
|
||||
return _invoke(run, api, args)
|
||||
|
||||
|
||||
def _validate_result(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
unknown = set(payload) - set(RESULT_DEFAULTS)
|
||||
if unknown:
|
||||
raise SystemExit(f"unknown result fields: {sorted(unknown)}")
|
||||
scope = payload.get("change_scope", "none")
|
||||
if scope not in CHANGE_SCOPES:
|
||||
raise SystemExit(f"change_scope must be one of {sorted(CHANGE_SCOPES)}")
|
||||
if payload.get("needs_code_change") and scope == "none":
|
||||
raise SystemExit(
|
||||
"needs_code_change=true requires an explicit change_scope "
|
||||
"(simulink_module / non_simulink_rtl / soc / hardware)"
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _validate_review(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
unknown = set(payload) - set(REVIEW_DEFAULTS)
|
||||
if unknown:
|
||||
raise SystemExit(f"unknown review fields: {sorted(unknown)}")
|
||||
if "approved" not in payload:
|
||||
raise SystemExit("review decision must set approved (true/false)")
|
||||
if not payload["approved"] and not payload.get("notes"):
|
||||
raise SystemExit("驳回必须写明理由:请提供 notes")
|
||||
return payload
|
||||
|
||||
|
||||
def cmd_record(args, api) -> int:
|
||||
run = RunDir(_resolve_work_dir(args), args.thread_id, create=False)
|
||||
pending = run.pending
|
||||
if not pending:
|
||||
raise SystemExit("nothing pending — run `start` or `resume` first")
|
||||
ledger = run.ledger
|
||||
by_key = {item["key"]: item for item in pending}
|
||||
|
||||
if args.all_ok:
|
||||
reviews = [k for k, v in by_key.items() if v.get("kind") == "review"]
|
||||
if reviews:
|
||||
raise SystemExit(
|
||||
"--all-ok 不能用于人工审核断点,必须由用户决定:" + ", ".join(reviews)
|
||||
)
|
||||
for key in by_key:
|
||||
ledger[key] = dict(RESULT_DEFAULTS)
|
||||
run.set_ledger(ledger)
|
||||
print(f"recorded default success for: {sorted(by_key)}")
|
||||
return 0
|
||||
|
||||
if not args.key:
|
||||
raise SystemExit("--key is required (or use --all-ok)")
|
||||
if args.key not in by_key:
|
||||
raise SystemExit(f"--key {args.key} is not pending; pending = {sorted(by_key)}")
|
||||
item = by_key[args.key]
|
||||
is_review = item.get("kind") == "review"
|
||||
|
||||
if args.approve or args.reject:
|
||||
if not is_review:
|
||||
raise SystemExit("--approve/--reject 只用于人工审核断点")
|
||||
payload: dict[str, Any] = {
|
||||
"approved": bool(args.approve),
|
||||
"notes": args.notes or "",
|
||||
}
|
||||
else:
|
||||
if args.result_file:
|
||||
raw = Path(args.result_file).read_text(encoding="utf-8")
|
||||
elif args.result is not None:
|
||||
raw = args.result
|
||||
else:
|
||||
raise SystemExit(
|
||||
"provide --approve/--reject (review) or --result '<json>' (subagent)"
|
||||
)
|
||||
payload = json.loads(raw)
|
||||
if not isinstance(payload, dict):
|
||||
raise SystemExit("result must be a JSON object")
|
||||
|
||||
payload = _validate_review(payload) if is_review else _validate_result(payload)
|
||||
ledger[args.key] = payload
|
||||
run.set_ledger(ledger)
|
||||
remaining = sorted(set(by_key) - set(ledger))
|
||||
print(f"recorded {args.key}: " + json.dumps(payload, ensure_ascii=False))
|
||||
print(f"still pending: {remaining if remaining else '(none — run `resume`)'}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_status(args, api) -> int:
|
||||
run = RunDir(_resolve_work_dir(args), args.thread_id, create=False)
|
||||
print(f"work dir: {run.work_dir}")
|
||||
print(f"run dir : {run.path}")
|
||||
if not run.path.exists():
|
||||
print("(no such run — check --work-dir / --thread-id)")
|
||||
return 1
|
||||
ledger = run.ledger
|
||||
print(f"ledger : {len(ledger)} recorded answer(s)")
|
||||
for key, value in ledger.items():
|
||||
print(" " + key + " -> " + json.dumps(value, ensure_ascii=False))
|
||||
pending = run.pending
|
||||
if pending:
|
||||
_print_pending(pending)
|
||||
else:
|
||||
state = run.state
|
||||
print("pending : none")
|
||||
if state:
|
||||
print(f"status : {state.get('status')}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_dry_run(args, api) -> int:
|
||||
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
||||
outcomes = payload.pop("outcomes", None)
|
||||
reviews = payload.pop("reviews", None)
|
||||
backend = api["ScriptedBackend"](outcomes) if outcomes else api["ScriptedBackend"]()
|
||||
gate = (
|
||||
api["ScriptedReviewGate"](reviews) if reviews else api["ScriptedReviewGate"]()
|
||||
)
|
||||
graph = api["build_workflow"]()
|
||||
state = graph.invoke(
|
||||
payload,
|
||||
{"recursion_limit": args.recursion_limit},
|
||||
context=api["WorkflowContext"](
|
||||
backend=backend, max_attempts=args.max_attempts, review_gate=gate
|
||||
),
|
||||
)
|
||||
print("scripted subagent calls:")
|
||||
for call in backend.calls:
|
||||
print(
|
||||
f" {call.task:<22} skill={call.skill or '-':<18} "
|
||||
f"write={call.allow_code_changes}"
|
||||
)
|
||||
print("human review checkpoints (auto-approved in rehearsal):")
|
||||
for request in gate.requests:
|
||||
print(f" {request.checkpoint:<22} {request.title}")
|
||||
_print_final(state, [])
|
||||
return 0 if state.get("status") == "completed" else 1
|
||||
|
||||
|
||||
def cmd_mermaid(args, api) -> int:
|
||||
print(api["build_workflow"]().get_graph().draw_mermaid())
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_schema(args, api) -> int:
|
||||
print("subagent result:")
|
||||
print(json.dumps(RESULT_DEFAULTS, ensure_ascii=False, indent=2))
|
||||
print("\nhuman review decision:")
|
||||
print(json.dumps(REVIEW_DEFAULTS, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception: # noqa: BLE001 - older streams
|
||||
pass
|
||||
|
||||
parser = argparse.ArgumentParser(prog="run_sim_rtl_flow")
|
||||
parser.add_argument("--project", type=Path, default=None)
|
||||
parser.add_argument(
|
||||
"--work-dir",
|
||||
default=None,
|
||||
help=(
|
||||
"the design project whose .sim_rtl_flow/ holds run state; "
|
||||
"defaults to cwd (on `start` it comes from the input's work_dir)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--thread-id", default="default")
|
||||
parser.add_argument("--recursion-limit", type=int, default=200)
|
||||
parser.add_argument("--max-attempts", type=int, default=10)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p = sub.add_parser("start")
|
||||
p.add_argument("--input", required=True)
|
||||
p.add_argument("--keep-ledger", action="store_true")
|
||||
p.set_defaults(func=cmd_start)
|
||||
|
||||
p = sub.add_parser("resume")
|
||||
p.set_defaults(func=cmd_resume)
|
||||
|
||||
p = sub.add_parser("record")
|
||||
p.add_argument("--key")
|
||||
p.add_argument("--result")
|
||||
p.add_argument("--result-file")
|
||||
p.add_argument("--approve", action="store_true")
|
||||
p.add_argument("--reject", action="store_true")
|
||||
p.add_argument("--notes")
|
||||
p.add_argument("--all-ok", action="store_true")
|
||||
p.set_defaults(func=cmd_record)
|
||||
|
||||
p = sub.add_parser("status")
|
||||
p.set_defaults(func=cmd_status)
|
||||
|
||||
p = sub.add_parser("dry-run")
|
||||
p.add_argument("--input", required=True)
|
||||
p.set_defaults(func=cmd_dry_run)
|
||||
|
||||
p = sub.add_parser("mermaid")
|
||||
p.set_defaults(func=cmd_mermaid)
|
||||
|
||||
p = sub.add_parser("schema")
|
||||
p.set_defaults(func=cmd_schema)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
if getattr(args, "approve", False) and getattr(args, "reject", False):
|
||||
raise SystemExit("--approve 与 --reject 不能同时使用")
|
||||
if args.project is None:
|
||||
args.project = _default_project()
|
||||
api = _bootstrap(args.project)
|
||||
return args.func(args, api)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
name: zynq-imx6-flow
|
||||
description: >
|
||||
Run the zynq-imx6-flow LangGraph workflow at E:\LangGraph_Design\zynq-imx6-flow —
|
||||
the closed-loop xmod (Zynq PS+PL + i.MX6) hardware-in-the-loop design/debug pipeline:
|
||||
cold power-cycle, connect JTAG, program bitstream/ELF or burn flash, restart the
|
||||
ps/imx6 app, analyze (ILA + PS/imx6 serial consoles), judge against design
|
||||
requirements, and iterate by modifying RTL / imx6 app / PS baremetal code. It drives
|
||||
the graph in-process (no langgraph dev) and hands the 6 interrupts (analyze /
|
||||
meets_requirement / propose / modify_fpga / modify_imx6 / modify_ps) back to this
|
||||
Claude session to execute. Also supports zynq-only boards (no i.MX6) and PS
|
||||
baremetal/petalinux modes.
|
||||
|
||||
ALWAYS use this skill whenever the user wants to run, start, drive, resume, or
|
||||
continue the zynq-imx6-flow workflow / xmod Zynq+imx6 design loop, or says things
|
||||
like "启动 zynq-imx6-flow", "跑 zynq 设计流程", "run the zynq FPGA flow", "执行 xmod
|
||||
设计任务", "继续 zynq 工作流", "resume the zynq design loop", even if they don't name
|
||||
the project path. Before driving, this skill confirms the langgraph conda env from
|
||||
/langgraph-vivado-install is in place.
|
||||
---
|
||||
|
||||
## What this skill does
|
||||
|
||||
Drives the **zynq-imx6-flow** graph (28 nodes / 39 flow-edges, 1:1 with
|
||||
`Notes/zynq-imx6-flow.excalidraw`, plan `Notes/Task00.md`). The deterministic
|
||||
hardware/check nodes (power, JTAG, program, restart) run in-process; the **6 interrupt
|
||||
nodes** pause and hand control to **this Claude session**, which does the real work
|
||||
(analysis, judgment, RTL/app/PS edits) and resumes the same thread. Persistence is a
|
||||
SqliteSaver file, so `start`/`resume` survive across separate process calls.
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Project root | 见 `~\skills_env.md` 的 `ZYNQ_IMX6_FLOW_ROOT`(driver 自动读取,也可 `--project-dir` 覆盖) |
|
||||
| Conda env | `langgraph` (path 见 `~\skills_env.md` 的 `CONDA_LANGGRAPH_PYTHON`;装环境用 `/langgraph-vivado-install`) |
|
||||
| Graph | `app.main_graph:build_graph` (in-process; no langgraph dev) |
|
||||
| Driver | `%USERPROFILE%\.claude\skills\zynq-imx6-flow\scripts\run_zynq_flow.py` |
|
||||
| Checkpoint | `<project>\runs\skill_checkpoint.sqlite` (keyed by `--thread-id`) |
|
||||
|
||||
**Drive the driver with the Bash tool (Git Bash), not PowerShell.** Windows PowerShell
|
||||
5.1 strips the double-quotes out of `--resume-json` payloads (see Gotchas), corrupting
|
||||
the JSON. Git Bash single-quoting passes it intact. All commands below are Bash and were
|
||||
run this way to verify them.
|
||||
|
||||
```bash
|
||||
PY="/c/Users/Administrator/miniconda3/envs/langgraph/python.exe"
|
||||
RUN="/c/Users/Administrator/.claude/skills/zynq-imx6-flow/scripts/run_zynq_flow.py"
|
||||
export PYTHONIOENCODING=utf-8
|
||||
```
|
||||
|
||||
## ⚠️ Hardware timing rule (power / USB-Blast)
|
||||
|
||||
**Before every power change to the board, USB-Blast must already be disconnected; only
|
||||
reconnect JTAG after power is up.** Never power the board up/down while USB-Blast is
|
||||
connected (protects the JTAG adapter and board). The graph topology already enforces
|
||||
this: `power_off → check_power_off →(是) blast_off_a → check_blast_off →(是) power_on →
|
||||
check_power →(是) blast_on → check_hw`. After `flash_burn` the graph returns to
|
||||
`power_off` for a full cold-restart before re-analyzing. Serial port numbers are **never
|
||||
hardcoded** — they come from `~/zynq_imx6_flow.conf` (see "Mock vs real").
|
||||
|
||||
## Step 0 — Confirm the environment (REQUIRED before driving)
|
||||
|
||||
The workflow runs in the `langgraph` conda env created by **`/langgraph-vivado-install`**.
|
||||
Verify it first so a missing env is a clear message, not a cryptic mid-run import error:
|
||||
|
||||
```bash
|
||||
"$PY" -c "from langgraph.checkpoint.sqlite import SqliteSaver; from langgraph.types import Command; import langgraph; print('[OK] langgraph')"
|
||||
```
|
||||
|
||||
- **`[OK]`** → proceed.
|
||||
- **import fails / no python** → tell the user to run **`/langgraph-vivado-install`**
|
||||
first (it creates the `langgraph` env with `langgraph` + `langgraph-checkpoint-sqlite`).
|
||||
Do not proceed until it succeeds.
|
||||
|
||||
Optional sanity check — the project's own acceptance suite (all mock, ~2 s):
|
||||
|
||||
```bash
|
||||
cd /e/LangGraph_Design/zynq-imx6-flow && "$PY" smoke_test.py # prints [PASS]
|
||||
```
|
||||
|
||||
## Step 1 — Start the workflow
|
||||
|
||||
Pick the board + first-pass program mode (defaults shown), then start on a fresh
|
||||
`--thread-id`:
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" start --thread-id zynq-<short-task-name> \
|
||||
--board-type xmod --ps-mode baremetal --program-mode bitstream --bit-path mock.bit
|
||||
```
|
||||
|
||||
- `--board-type` `xmod` (Zynq+i.MX6, default) | `zynq_only` (no i.MX6 — imx6 branch is skipped).
|
||||
- `--ps-mode` `baremetal` (default) | `petalinux`.
|
||||
- `--program-mode` `bitstream` (default, downloads bit → restart → analyze) |
|
||||
`debug_flash` (image already fused in flash → straight to analyze, no download).
|
||||
- `--bit-path` / `--elf-path` / `--flash-path` optional artifact paths.
|
||||
- Add `--real` only with hardware wired + board connected (default is MOCK — see below).
|
||||
|
||||
The driver prints **one JSON line** and exits:
|
||||
|
||||
| Exit | `status` | Meaning |
|
||||
|---|---|---|
|
||||
| 0 | `done` | Workflow finished (符合要求 / iteration cap) → Step 3 |
|
||||
| 1 | `error` | Read `error`, diagnose, report |
|
||||
| 2 | `interrupt` | An interrupt fired → read `interrupt.action` (and `pending_count`) → Step 2 |
|
||||
|
||||
## Step 2 — Handle the interrupt(s), then resume
|
||||
|
||||
On exit 2, `interrupt.action` (or the `pending_interrupts` list) tells you what to do.
|
||||
Perform it, then resume the **same `--thread-id`**. Repeat until exit 0.
|
||||
|
||||
| `action` | excalidraw node | What you (the session) do | Resume JSON |
|
||||
|---|---|---|---|
|
||||
| `analyze` | 通过ila/串口分析 | Grab ILA + the `sources` consoles (ps_console, and imx6_console if xmod); summarize | `{"analysis_log":"<摘要/路径>"}` |
|
||||
| `meets_requirement` | 符合设计要求? | Judge analysis vs the design requirement | `{"meets": true}` or `{"meets": false}` |
|
||||
| `propose` | 分析原因,提方案 | Diagnose; **review the plan with `/langgraph-plan-review`**; decide what to change + next program mode | `{"reason":"...","need_fpga":bool,"need_imx6":bool,"need_ps":bool,"program_mode":"bitstream"或"flash"}` |
|
||||
| `modify_fpga` | 改 RTL→综合→bit/flash | Edit RTL, run Vivado synth/impl, produce bit + flash | `{"bit_path":"...","flash_path":"..."}` |
|
||||
| `modify_imx6` | VM 改 app→交叉编译 | In the Ubuntu VM edit imx6 app + cross-compile | `{"app_path":"..."}` |
|
||||
| `modify_ps` | sdk 改 PS 裸机→ELF | In Xilinx SDK edit PS baremetal code + build (petalinux: rebuild app/image) | `{"elf_path":"..."}` |
|
||||
|
||||
**Single interrupt** — resume with the payload directly:
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" resume --thread-id zynq-<name> --resume-json '{"meets": false}'
|
||||
```
|
||||
|
||||
**Three-way parallel fan-out** — after a `propose` that sets `need_fpga`/`need_imx6`/
|
||||
`need_ps` true, `modify_fpga` + `modify_imx6` + `modify_ps` fire **in one superstep**
|
||||
(`pending_count: 3`). Resume them with **one map keyed by `action`** (subset allowed;
|
||||
unresumed ones stay pending and the deferred `round_done` still waits for every branch):
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" resume --thread-id zynq-<name> --resume-json \
|
||||
'{"modify_fpga":{"bit_path":"new.bit","flash_path":"new.bin"},"modify_imx6":{"app_path":"new-app"},"modify_ps":{"elf_path":"new.elf"}}'
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Always reuse the **same `--thread-id`** from `start` — that's how the sqlite checkpoint
|
||||
reattaches (different tasks → different thread ids → isolated state).
|
||||
- `propose` fans out to `decide_fpga`/`decide_imx6`/`decide_ps`; all-false goes straight
|
||||
to `round_done` → re-program. `board_type=zynq_only` forces `need_imx6=false`.
|
||||
- `program_mode` from `propose` chooses the re-program path after `round_done`:
|
||||
`bitstream` = download bit; `flash` = burn flash then full cold power/connect
|
||||
re-sequence (`flash_burn → power_off`). This is the big closed loop.
|
||||
- `"$PY" "$RUN" status --thread-id zynq-<name>` re-prints the current state without advancing.
|
||||
|
||||
## Step 3 — Finish
|
||||
|
||||
On exit 0 (`status: done`), report iterations completed and whether the design met
|
||||
requirements (`meets_requirement`) or hit the iteration cap. Offer to inspect
|
||||
`<project>\runs\` artifacts or the per-step `history`.
|
||||
|
||||
To abandon a paused run and start clean, delete `<project>\runs\skill_checkpoint.sqlite`
|
||||
or use a fresh `--thread-id`.
|
||||
|
||||
## Mock vs real
|
||||
|
||||
- **MOCK (default)** — sets `ZYNQ_IMX6_MOCK=1`; hardware/check nodes return success
|
||||
immediately. Dry-runs the *design-loop logic* and all 6 interrupts with no board. Ideal
|
||||
for demonstrating/validating the flow. Every command in this doc was verified in mock.
|
||||
- **Real (`--real`)** — sets `ZYNQ_IMX6_MOCK=0` and drives actual hardware. The `tools_hw`
|
||||
paths are wired (Modbus power relay `0x05`, USB-Blast `0xA0` frames over pyserial). Real
|
||||
mode additionally requires:
|
||||
1. **pyserial** in the env (`"$PY" -m pip install pyserial`) — not installed by default;
|
||||
without it `--real` returns `{"status":"error","error":"Real mode requires pyserial"}`.
|
||||
2. **`~/zynq_imx6_flow.conf`** with 4 sections `[power] [usb_blast] [imx6_console]
|
||||
[ps_console]` (template: project's `zynq_imx6_flow.example.conf`). Relay ports
|
||||
(`power`/`usb_blast`) may fall back to `~/vivado_mcp_usb.conf`. `zynq_only` boards
|
||||
don't need `[imx6_console]`. COM numbers differ per machine — find them in Device
|
||||
Manager → Ports (COM & LPT).
|
||||
3. A connected board + Vivado for the JTAG program/flash nodes.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **PowerShell 5.1 corrupts `--resume-json`.** `& $py $RUN resume --resume-json '{"meets": false}'`
|
||||
arrives at Python as `{meets: false}` — PS strips the double-quotes (and `\"`-escaping
|
||||
splits it on spaces instead). This is the known PSNativeCommandArgumentPassing bug,
|
||||
unfixed in 5.1. **Use the Bash tool.** Verified: the same JSON through Git Bash arrives
|
||||
intact (`analysis_log` values with spaces round-trip correctly).
|
||||
- **`round_done` is a deferred node** (`defer=True`). With the three-way fan-out it runs
|
||||
**exactly once** after all active modify/deploy branches finish (verified: iteration
|
||||
advances 0→1 once even with all three branches on). Don't expect it after only the
|
||||
first branch.
|
||||
- **`propose` must return `program_mode` ∈ {bitstream, flash}.** Any other value raises
|
||||
`propose returned invalid program_mode`. (The `start` `--program-mode` has a third
|
||||
option `debug_flash` for the first pass only; `propose` does not.)
|
||||
- **`langgraph.__version__` doesn't exist** on 1.2.9 — check the version via
|
||||
`importlib.metadata.version("langgraph")` if needed (smoke_test asserts `>= 0.2.39`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---|---|
|
||||
| `--resume-json` value shows up without quotes / split on spaces | You used PowerShell. Drive via the Bash tool (single-quoted JSON). |
|
||||
| `UnicodeEncodeError: 'gbk' ...` | Don't use bare `conda run`; use the env `python.exe` directly (the driver reconfigures stdout to UTF-8). |
|
||||
| `{"status":"error","error":"Real mode requires pyserial"}` | `--real` needs pyserial: `"$PY" -m pip install pyserial`. Or drop `--real` for a mock run. |
|
||||
| `SerialConfigError: Missing serial sections [...]` | Create `~/zynq_imx6_flow.conf` from `zynq_imx6_flow.example.conf` (relay ports may come from `~/vivado_mcp_usb.conf`). |
|
||||
| `import failed ... Run /langgraph-vivado-install` | The `langgraph` env is missing/incomplete → run `/langgraph-vivado-install`. |
|
||||
| `propose returned invalid program_mode` | The `propose` resume used something other than `bitstream`/`flash`. |
|
||||
| `resume` seems to start over | You passed a different `--thread-id` than `start`. Use the same one. |
|
||||
| Want to see the graph visually | Optional: `langgraph dev` via the project's `langgraph.json` opens Studio — not required to run. |
|
||||
@@ -0,0 +1,187 @@
|
||||
"""In-process driver for the zynq-imx6-flow LangGraph workflow.
|
||||
|
||||
Builds the compiled graph from the project's ``app`` package and drives it with a
|
||||
persistent SqliteSaver — no ``langgraph dev`` server. Mirrors the sibling
|
||||
fpga-imx6-flow driver: exit code 0 = done, 1 = error, 2 = interrupt fired.
|
||||
|
||||
Commands:
|
||||
start --project-dir <dir> [--program-mode bitstream|debug_flash]
|
||||
[--ps-mode baremetal|petalinux] [--board-type xmod|zynq_only]
|
||||
[--bit-path ..] [--elf-path ..] [--flash-path ..]
|
||||
[--thread-id ..] [--real] [--max-iterations N]
|
||||
resume --project-dir <dir> --resume-json '<json>' [--thread-id ..] [--real]
|
||||
status --project-dir <dir> [--thread-id ..]
|
||||
|
||||
Output: one JSON line on stdout describing status + the pending interrupt(s).
|
||||
|
||||
Resume payloads:
|
||||
* Single pending interrupt -> pass its payload directly, e.g. '{"meets": true}'.
|
||||
* Multiple pending (the three-way modify fan-out: modify_fpga + modify_imx6 +
|
||||
modify_ps can fire in one superstep) -> pass a map keyed by each interrupt's
|
||||
"action", e.g. '{"modify_fpga": {...}, "modify_imx6": {...}, "modify_ps": {...}}'.
|
||||
You may resume a subset; unresumed interrupts stay pending and round_done
|
||||
(defer=True) still waits for every active branch.
|
||||
|
||||
Run via the env python directly or ``conda run --no-capture-output`` to avoid the
|
||||
Windows GBK console re-encode crash on the Chinese interrupt payloads.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8") # emit UTF-8 even on a GBK console
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _env_lookup(key: str, default: str | None = None) -> str | None:
|
||||
"""Read KEY from the OS environment, else from a ``KEY = value`` line in
|
||||
~/skills_env.md. Machine-local paths are never hardcoded in a skill; see
|
||||
the same helper in fpga_flash/scripts/vivado_env.py."""
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
return value
|
||||
path = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
match = re.match(rf"\s*{re.escape(key)}\s*=\s*(.+?)\s*(?:#.*)?$", line)
|
||||
if match:
|
||||
found = match.group(1).strip()
|
||||
if found and not found.startswith("<"):
|
||||
return found
|
||||
except OSError:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
#: The zynq-imx6-flow checkout: ZYNQ_IMX6_FLOW_ROOT in ~/skills_env.md.
|
||||
DEFAULT_PROJECT = _env_lookup("ZYNQ_IMX6_FLOW_ROOT")
|
||||
|
||||
|
||||
def _emit(obj: dict, code: int) -> None:
|
||||
print(json.dumps(obj, ensure_ascii=False))
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def _pending(snap):
|
||||
return [intr for task in snap.tasks for intr in task.interrupts]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Drive the zynq-imx6-flow graph in-process.")
|
||||
ap.add_argument("command", choices=["start", "resume", "status"])
|
||||
ap.add_argument("--project-dir", default=DEFAULT_PROJECT)
|
||||
ap.add_argument("--thread-id", default="zynq-skill-1")
|
||||
ap.add_argument("--resume-json", default="")
|
||||
ap.add_argument("--program-mode", default="bitstream",
|
||||
choices=["bitstream", "debug_flash"])
|
||||
ap.add_argument("--ps-mode", default="baremetal", choices=["baremetal", "petalinux"])
|
||||
ap.add_argument("--board-type", default="xmod", choices=["xmod", "zynq_only"])
|
||||
ap.add_argument("--bit-path", default="")
|
||||
ap.add_argument("--elf-path", default="")
|
||||
ap.add_argument("--flash-path", default="")
|
||||
ap.add_argument("--real", action="store_true",
|
||||
help="drive real hardware (tools_* must be wired; default is MOCK dry-run)")
|
||||
ap.add_argument("--max-iterations", type=int, default=8)
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.project_dir:
|
||||
_emit({"status": "error",
|
||||
"error": "zynq-imx6-flow 仓库路径未配置:在 ~/skills_env.md 增加 "
|
||||
"`ZYNQ_IMX6_FLOW_ROOT = <检出目录>`,或用 --project-dir 指定"}, 1)
|
||||
project = Path(args.project_dir)
|
||||
if not (project / "app" / "main_graph.py").exists():
|
||||
_emit({"status": "error",
|
||||
"error": f"app/main_graph.py not found under {project}; check --project-dir"}, 1)
|
||||
|
||||
# MOCK by default so the design loop can be dry-run with no hardware attached.
|
||||
os.environ["ZYNQ_IMX6_MOCK"] = "0" if args.real else "1"
|
||||
sys.path.insert(0, str(project))
|
||||
|
||||
try:
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.types import Command
|
||||
from app.config import initial_state
|
||||
from app.main_graph import build_graph
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_emit({"status": "error",
|
||||
"error": f"import failed: {exc}. Run /langgraph-vivado-install to set up the env."}, 1)
|
||||
|
||||
runs = project / "runs"
|
||||
runs.mkdir(exist_ok=True)
|
||||
db = str(runs / "skill_checkpoint.sqlite")
|
||||
cfg = {"configurable": {"thread_id": args.thread_id}}
|
||||
|
||||
with SqliteSaver.from_conn_string(db) as cp:
|
||||
graph = build_graph(checkpointer=cp)
|
||||
try:
|
||||
if args.command == "start":
|
||||
init = initial_state(
|
||||
program_mode=args.program_mode,
|
||||
ps_mode=args.ps_mode,
|
||||
board_type=args.board_type,
|
||||
max_iterations=args.max_iterations,
|
||||
)
|
||||
if args.bit_path:
|
||||
init["bit_path"] = args.bit_path
|
||||
if args.elf_path:
|
||||
init["elf_path"] = args.elf_path
|
||||
if args.flash_path:
|
||||
init["flash_path"] = args.flash_path
|
||||
graph.invoke(init, cfg)
|
||||
elif args.command == "resume":
|
||||
payload = json.loads(args.resume_json) if args.resume_json else {}
|
||||
pend = _pending(graph.get_state(cfg))
|
||||
if len(pend) <= 1:
|
||||
graph.invoke(Command(resume=payload), cfg)
|
||||
else:
|
||||
# Multiple interrupts pending (three-way modify fan-out). payload
|
||||
# must be a map keyed by each interrupt's "action" (or its id).
|
||||
resume_map = {}
|
||||
for intr in pend:
|
||||
val = intr.value if isinstance(intr.value, dict) else {}
|
||||
action = val.get("action")
|
||||
if action in payload:
|
||||
resume_map[intr.id] = payload[action]
|
||||
elif intr.id in payload:
|
||||
resume_map[intr.id] = payload[intr.id]
|
||||
if not resume_map:
|
||||
_emit({"status": "error",
|
||||
"error": f"resume-json keys {list(payload)} matched none of the "
|
||||
f"pending actions {[i.value.get('action') for i in pend]}"}, 1)
|
||||
graph.invoke(Command(resume=resume_map), cfg)
|
||||
snap = graph.get_state(cfg)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_emit({"status": "error", "error": str(exc)}, 1)
|
||||
|
||||
pend = _pending(snap)
|
||||
if snap.next:
|
||||
_emit({
|
||||
"status": "interrupt",
|
||||
"thread_id": args.thread_id,
|
||||
"mock": not args.real,
|
||||
"next": list(snap.next),
|
||||
"iteration": snap.values.get("iteration"),
|
||||
"interrupt": pend[0].value if pend else None,
|
||||
"pending_interrupts": [i.value for i in pend],
|
||||
"pending_count": len(pend),
|
||||
}, 2)
|
||||
else:
|
||||
_emit({
|
||||
"status": "done",
|
||||
"thread_id": args.thread_id,
|
||||
"iteration": snap.values.get("iteration"),
|
||||
"meets_requirement": snap.values.get("meets_requirement"),
|
||||
"steps": len(snap.values.get("history", [])),
|
||||
}, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user