Skip to content

Commit f9c1d2b

Browse files
Run agent-semantic eval policies in parallel and surface results in Job Summary.
Each matrix shard boots Kind with the target POLICY/PRIOR_MIX, then merge writes the compare table to the Actions Summary tab. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 73b22df commit f9c1d2b

8 files changed

Lines changed: 230 additions & 31 deletions

File tree

.github/workflows/e2e-kind.yaml

Lines changed: 150 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,31 @@ jobs:
6767
KIND_CLUSTER_NAME: actordock-functional
6868
run: ./hack/kind-down.sh || true
6969

70-
# agent-semantic@v2 ablation: random | resource-evict | semantic L1 | semantic L1+L3
70+
# Parallel ablation: one Kind cluster per policy variant (no mid-run POLICY switch).
7171
e2e-eval:
7272
runs-on: ubuntu-latest
73-
timeout-minutes: 120
73+
timeout-minutes: 90
74+
strategy:
75+
fail-fast: false
76+
matrix:
77+
include:
78+
- label: random
79+
policy: random
80+
prior_mix: ""
81+
cluster: actordock-eval-random
82+
- label: resource-evict
83+
policy: resource-evict
84+
prior_mix: ""
85+
cluster: actordock-eval-re
86+
- label: semantic-score-l1
87+
policy: semantic-score
88+
prior_mix: "0"
89+
cluster: actordock-eval-l1
90+
- label: semantic-score
91+
policy: semantic-score
92+
prior_mix: "0.3"
93+
cluster: actordock-eval-full
94+
name: e2e-eval (${{ matrix.label }})
7495
steps:
7596
- uses: actions/checkout@v4
7697
- uses: actions/setup-go@v5
@@ -88,15 +109,16 @@ jobs:
88109
89110
- name: Create cluster and deploy
90111
env:
91-
KIND_CLUSTER_NAME: actordock-eval
92-
POLICY: fifo
112+
KIND_CLUSTER_NAME: ${{ matrix.cluster }}
113+
POLICY: ${{ matrix.policy }}
114+
SEMANTIC_PRIOR_MIX: ${{ matrix.prior_mix }}
93115
run: |
94116
chmod +x hack/*.sh hack/replay-agent-semantic.py
95117
./hack/kind-up.sh
96118
97-
- name: Agent-semantic ablation (8 agents / 2 workers)
119+
- name: Agent-semantic replay (8 agents / 2 workers)
98120
env:
99-
KIND_CLUSTER_NAME: actordock-eval
121+
KIND_CLUSTER_NAME: ${{ matrix.cluster }}
100122
E2E_SUITE: agent-semantic
101123
ACTORDOCK_API: http://127.0.0.1:18080
102124
EVAL_OUT_DIR: docs/eval/results
@@ -105,29 +127,140 @@ jobs:
105127
AGENT_SEMANTIC_MIN_WORKERS: "2"
106128
AGENT_SEMANTIC_SPEED: "60"
107129
AGENT_SEMANTIC_MIN_LOCK: "0.25"
108-
AGENT_SEMANTIC_POLICIES: "random,resource-evict,semantic-score-l1,semantic-score"
130+
AGENT_SEMANTIC_POLICIES: ${{ matrix.label }}
131+
AGENT_SEMANTIC_SWITCH_POLICY: "0"
109132
run: ./hack/verify-local.sh
110133

111-
- name: Upload agent-semantic results
134+
- name: Publish policy summary on Actions page
135+
if: success() || failure()
136+
env:
137+
LABEL: ${{ matrix.label }}
138+
run: |
139+
python3 - <<'PY'
140+
import json, os
141+
from pathlib import Path
142+
label = os.environ["LABEL"]
143+
out = Path(os.environ.get("GITHUB_STEP_SUMMARY", "/dev/null"))
144+
p = Path(f"docs/eval/results/agent_semantic_v2__{label}.json")
145+
lines = [f"## agent-semantic `{label}`", ""]
146+
if not p.exists():
147+
lines += [f"_missing {p}_", ""]
148+
else:
149+
r = json.loads(p.read_text())
150+
lines += [
151+
"| metric | value |",
152+
"|--------|------:|",
153+
f"| sessions ok/fail | {r.get('sessions_ok')}/{r.get('sessions_failed')} |",
154+
f"| mid_tool | {r.get('mid_tool_suspend', 0)} |",
155+
f"| mid_tool_rate | {float(r.get('mid_tool_rate') or 0):.2f} |",
156+
f"| suspend | {r.get('suspend_total', 0)} |",
157+
f"| hard_rate | {float(r.get('victim_l3_hard_rate') or 0):.2f} |",
158+
f"| vic_hard / vic_easy | {r.get('victim_l3_hard', 0)} / {r.get('victim_l3_easy', 0)} |",
159+
f"| resume_hard_s / resume_easy_s | {float(r.get('resume_sec_l3_hard') or 0):.3f} / {float(r.get('resume_sec_l3_easy') or 0):.3f} |",
160+
f"| victim_cx | {float(r.get('victim_complexity_mean') or 0):.3f} |",
161+
f"| resume_wait_s | {float(r.get('resume_wait_mean_s') or 0):.3f} |",
162+
f"| wall_s | {float(r.get('wall_sec') or 0):.1f} |",
163+
"",
164+
f"victims=`{r.get('victim_by_cohort')}`",
165+
"",
166+
]
167+
out.write_text("\n".join(lines) + "\n")
168+
print("".join(line + "\n" for line in lines))
169+
PY
170+
171+
- name: Upload policy result
112172
if: success() || failure()
113173
uses: actions/upload-artifact@v4
114174
with:
115-
name: agent-semantic-policy-compare
116-
path: |
117-
docs/eval/results/policy_compare_agent_semantic_v2.md
118-
docs/eval/results/agent_semantic_v2__*.json
175+
name: agent-semantic-${{ matrix.label }}
176+
path: docs/eval/results/agent_semantic_v2__${{ matrix.label }}.json
119177
if-no-files-found: warn
120178

121179
- name: Dump on failure
122180
if: failure()
123181
run: |
124-
kubectl --context kind-actordock-eval -n actordock get pods -o wide || true
125-
kubectl --context kind-actordock-eval -n actordock describe pods || true
126-
kubectl --context kind-actordock-eval -n actordock logs deploy/controlplane --all-containers=true || true
127-
kubectl --context kind-actordock-eval -n actordock logs statefulset/worker --all-containers=true || true
182+
kubectl --context kind-${{ matrix.cluster }} -n actordock get pods -o wide || true
183+
kubectl --context kind-${{ matrix.cluster }} -n actordock describe pods || true
184+
kubectl --context kind-${{ matrix.cluster }} -n actordock logs deploy/controlplane --all-containers=true || true
185+
kubectl --context kind-${{ matrix.cluster }} -n actordock logs statefulset/worker --all-containers=true || true
128186
129187
- name: Teardown
130188
if: always()
131189
env:
132-
KIND_CLUSTER_NAME: actordock-eval
190+
KIND_CLUSTER_NAME: ${{ matrix.cluster }}
133191
run: ./hack/kind-down.sh || true
192+
193+
e2e-eval-merge:
194+
runs-on: ubuntu-latest
195+
needs: e2e-eval
196+
if: always() && !cancelled()
197+
steps:
198+
- uses: actions/checkout@v4
199+
- uses: actions/setup-python@v5
200+
with:
201+
python-version: "3.12"
202+
203+
- name: Download policy artifacts
204+
uses: actions/download-artifact@v4
205+
with:
206+
pattern: agent-semantic-*
207+
path: docs/eval/results/parts
208+
merge-multiple: true
209+
210+
- name: Merge compare table + gate
211+
env:
212+
EVAL_OUT_DIR: docs/eval/results
213+
run: |
214+
chmod +x hack/replay-agent-semantic.py
215+
mkdir -p docs/eval/results
216+
# Flatten downloaded JSONs into results/
217+
find docs/eval/results/parts -name 'agent_semantic_v2__*.json' -exec cp -f {} docs/eval/results/ \;
218+
ls -la docs/eval/results/agent_semantic_v2__*.json
219+
python3 hack/replay-agent-semantic.py --merge-only --out docs/eval/results
220+
python3 - <<'PY'
221+
import json, os, sys
222+
from pathlib import Path
223+
out = Path("docs/eval/results")
224+
want = ["random", "resource-evict", "semantic-score-l1", "semantic-score"]
225+
failed = []
226+
missing = []
227+
for label in want:
228+
p = out / f"agent_semantic_v2__{label}.json"
229+
if not p.exists():
230+
missing.append(label)
231+
continue
232+
r = json.loads(p.read_text())
233+
if int(r.get("sessions_failed") or 0) != 0 or int(r.get("sessions_ok") or 0) <= 0:
234+
failed.append(f"{p.name}: ok={r.get('sessions_ok')} fail={r.get('sessions_failed')}")
235+
compare = out / "policy_compare_agent_semantic_v2.md"
236+
if not compare.exists():
237+
sys.exit("missing policy_compare_agent_semantic_v2.md")
238+
body = compare.read_text()
239+
print(body)
240+
# Show full compare on the Actions run Summary tab (no artifact download needed).
241+
summary = Path(os.environ.get("GITHUB_STEP_SUMMARY", ""))
242+
if str(summary):
243+
with summary.open("a", encoding="utf-8") as f:
244+
f.write(body)
245+
if missing:
246+
f.write("\n\n**MISSING:** " + ", ".join(missing) + "\n")
247+
if failed:
248+
f.write("\n\n**FAILED:**\n\n- " + "\n- ".join(failed) + "\n")
249+
if missing:
250+
print("MISSING:", ", ".join(missing))
251+
sys.exit(1)
252+
if failed:
253+
print("FAILED:", *failed, sep="\n ")
254+
sys.exit(1)
255+
print(f"ok: {len(want)} policy report(s)")
256+
PY
257+
258+
- name: Upload merged compare (optional archive)
259+
if: success() || failure()
260+
uses: actions/upload-artifact@v4
261+
with:
262+
name: agent-semantic-policy-compare
263+
path: |
264+
docs/eval/results/policy_compare_agent_semantic_v2.md
265+
docs/eval/results/agent_semantic_v2__*.json
266+
if-no-files-found: warn

docs/eval/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ Each dataset version directory should include:
3232
- Metrics table aligned with `docs/research/metrics.md`
3333
- Optional: raw decision logs for debugging
3434

35-
CI job `e2e-eval` runs `E2E_SUITE=agent-semantic`: 8 agents / 2 workers on
36-
`datasets/agent-semantic@v2`; policies
37-
`random,resource-evict,semantic-score-l1,semantic-score` (L3 ablation);
38-
artifact `agent-semantic-policy-compare`.
35+
CI job `e2e-eval` runs `E2E_SUITE=agent-semantic` in a **matrix** (one Kind cluster per
36+
policy: `random`, `resource-evict`, `semantic-score-l1`, `semantic-score`);
37+
Primary view: GitHub Actions **Summary** tab (full compare table + per-policy metrics).
38+
Artifacts remain as optional archive.
3939

4040
Do not commit huge binary checkpoints here; link or use Git LFS if needed.

docs/eval/agent-semantic-workload.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ Refuse to publish the package if:
198198
|------|------|-----|
199199
| **A. Live Kind replay** | Primary claim for Actordock | Port-forward CP; for each policy, Create/Resume sandboxes on `arrival_ts` schedule; drive agent (or phase-faithful stub) from `tool_trace`; POST L1/L3 signals; collect `/metrics` + victim logs |
200200
| **B. Offline decision replay** | Fast ablation of keepScore | Feed cached signals + arrivals into a Place/Evict simulator that embeds `semantic-score` / baselines; no gVisor |
201-
| **C. CI agent-semantic** | PR / main Kind job | `E2E_SUITE=agent-semantic`: 8 agents / 2 workers; ablation matrix (random, resource-evict, semantic-score-l1, semantic-score) |
201+
| **C. CI agent-semantic** | PR / main Kind job | Matrix: one cluster per policy (`random`, `resource-evict`, `semantic-score-l1`, `semantic-score`); merge job builds compare table |
202202

203203
Primary paper/demo numbers should come from **Mode A** (or A+B agreement).
204204

docs/eval/results/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Eval results
22

3-
Generated by `hack/replay-agent-semantic.py` (CI job `e2e-eval` uploads artifact
4-
`agent-semantic-policy-compare`). Artifacts are gitignored except this README.
3+
Generated by `hack/replay-agent-semantic.py`. In CI, the compare table is written to the
4+
Actions **Summary** tab (Job Summary); artifacts are optional.
55

66
- `policy_compare_agent_semantic_v2.md` — cross-policy table
77
- `agent_semantic_v2__<policy>.json` — per-policy metrics

e2e/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ E2E_SUITE=agent-semantic ./hack/verify-local.sh # dataset policy compare
2323
E2E_SUITE=all ./hack/verify-local.sh # both
2424
```
2525

26-
CI: **functional** + **e2e-eval** (`random` / `resource-evict` / `semantic-score-l1` / `semantic-score`).
26+
CI: **functional** + **e2e-eval** matrix (`random` / `resource-evict` / `semantic-score-l1` / `semantic-score` in parallel) + merge compare.
2727

2828
## Functional suite
2929

@@ -54,5 +54,5 @@ CI: **functional** + **e2e-eval** (`random` / `resource-evict` / `semantic-score
5454

5555
`semantic-score-l1` = L1 lock only (`SEMANTIC_PRIOR_MIX=0`); `semantic-score` = L1+L3 (`0.3`).
5656

57-
Outputs `docs/eval/results/agent_semantic_v2__*.json` + `policy_compare_agent_semantic_v2.md`.
57+
Outputs also appear on the Actions **Summary** tab (Job Summary); artifacts are optional archive only.
5858
Gate: every policy `sessions_failed=0`.

hack/kind-up.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ echo "==> load into kind"
3333
kind load docker-image actordock/controlplane:dev --name "${CLUSTER_NAME}"
3434
kind load docker-image actordock/worker:dev --name "${CLUSTER_NAME}"
3535

36-
echo "==> deploy rustfs + actordock (POLICY=${POLICY})"
36+
echo "==> deploy rustfs + actordock (POLICY=${POLICY}${SEMANTIC_PRIOR_MIX:+ PRIOR_MIX=${SEMANTIC_PRIOR_MIX}})"
3737
kubectl apply -f "${ROOT}/manifests/kind/rustfs.yaml"
3838
kubectl -n actordock rollout status deploy/rustfs --timeout=180s
3939
# Recreate bucket-init job so re-runs are idempotent.
@@ -42,7 +42,11 @@ kubectl apply -f "${ROOT}/manifests/kind/rustfs.yaml"
4242
kubectl -n actordock wait --for=condition=complete job/rustfs-bucket-init --timeout=180s
4343

4444
kubectl apply -f "${ROOT}/manifests/kind/actordock.yaml"
45-
kubectl -n actordock set env deployment/controlplane POLICY="${POLICY}"
45+
CP_ENV=(POLICY="${POLICY}")
46+
if [[ -n "${SEMANTIC_PRIOR_MIX:-}" ]]; then
47+
CP_ENV+=("SEMANTIC_PRIOR_MIX=${SEMANTIC_PRIOR_MIX}")
48+
fi
49+
kubectl -n actordock set env deployment/controlplane "${CP_ENV[@]}"
4650
# Ensure pods pick up freshly loaded images.
4751
kubectl -n actordock rollout restart deployment/controlplane deployment/redis
4852
kubectl -n actordock rollout restart statefulset/worker

hack/replay-agent-semantic.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import urllib.request
3838
from collections import defaultdict
3939
from concurrent.futures import ThreadPoolExecutor, as_completed
40-
from dataclasses import dataclass, field
40+
from dataclasses import dataclass, field, fields as dc_fields
4141
from datetime import datetime, timezone
4242
from pathlib import Path
4343
from typing import Any
@@ -923,6 +923,42 @@ def run_policy(
923923
return result
924924

925925

926+
def load_policy_result(path: Path) -> PolicyResult:
927+
raw = json.loads(path.read_text())
928+
allowed = {f.name for f in dc_fields(PolicyResult)}
929+
return PolicyResult(**{k: v for k, v in raw.items() if k in allowed})
930+
931+
932+
COMPARE_ORDER = (
933+
"random",
934+
"resource-evict",
935+
"semantic-score-l1",
936+
"semantic-score",
937+
"fifo",
938+
"lru-idle",
939+
)
940+
941+
942+
def merge_results_dir(out: Path) -> Path:
943+
"""Load agent_semantic_v2__*.json under out and write policy_compare markdown."""
944+
files = sorted(out.glob("agent_semantic_v2__*.json"))
945+
if not files:
946+
raise SystemExit(f"no agent_semantic_v2__*.json under {out}")
947+
by_label = {p.stem.replace("agent_semantic_v2__", "", 1): load_policy_result(p) for p in files}
948+
ordered: list[PolicyResult] = []
949+
seen: set[str] = set()
950+
for label in COMPARE_ORDER:
951+
if label in by_label:
952+
ordered.append(by_label[label])
953+
seen.add(label)
954+
for label, res in sorted(by_label.items()):
955+
if label not in seen:
956+
ordered.append(res)
957+
compare = out / "policy_compare_agent_semantic_v2.md"
958+
write_compare(ordered, compare)
959+
return compare
960+
961+
926962
def write_compare(results: list[PolicyResult], path: Path) -> None:
927963
lines = [
928964
"# agent-semantic policy compare",
@@ -1018,8 +1054,20 @@ def main() -> None:
10181054
ap.add_argument("--resume-timeout", type=float, default=300.0)
10191055
ap.add_argument("--out", type=Path, default=DEFAULT_OUT)
10201056
ap.add_argument("--dry-run", action="store_true", help="Load dataset only")
1057+
ap.add_argument(
1058+
"--merge-only",
1059+
action="store_true",
1060+
help="Only merge agent_semantic_v2__*.json under --out into policy_compare markdown",
1061+
)
10211062
args = ap.parse_args()
10221063

1064+
if args.merge_only:
1065+
args.out.mkdir(parents=True, exist_ok=True)
1066+
compare = merge_results_dir(args.out)
1067+
log(f"[merge] {compare}")
1068+
sys.stdout.write(compare.read_text())
1069+
return
1070+
10231071
sessions_path = args.dataset / "sessions.jsonl"
10241072
if not sessions_path.exists():
10251073
raise SystemExit(f"missing {sessions_path}")

hack/verify-local.sh

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,26 @@ run_agent_semantic() {
5858
ensure_api_pf
5959
mkdir -p "${EVAL_OUT_DIR}"
6060
chmod +x "${ROOT}/hack/replay-agent-semantic.py"
61-
echo "==> agent-semantic replay limit=${AGENT_SEMANTIC_LIMIT} inflight=${AGENT_SEMANTIC_INFLIGHT} policies=${AGENT_SEMANTIC_POLICIES}"
61+
SWITCH_ARGS=()
62+
# Default: switch when multiple policies; CI matrix sets AGENT_SEMANTIC_SWITCH_POLICY=0.
63+
switch="${AGENT_SEMANTIC_SWITCH_POLICY:-}"
64+
if [[ -z "${switch}" ]]; then
65+
IFS=',' read -r -a _pols <<< "${AGENT_SEMANTIC_POLICIES}"
66+
if ((${#_pols[@]} > 1)); then
67+
switch=1
68+
else
69+
switch=0
70+
fi
71+
fi
72+
if [[ "${switch}" == "1" || "${switch}" == "true" ]]; then
73+
SWITCH_ARGS=(--switch-policy)
74+
fi
75+
echo "==> agent-semantic replay limit=${AGENT_SEMANTIC_LIMIT} inflight=${AGENT_SEMANTIC_INFLIGHT} policies=${AGENT_SEMANTIC_POLICIES} switch=${switch}"
6276
python3 "${ROOT}/hack/replay-agent-semantic.py" \
6377
--api "${ACTORDOCK_API}" \
6478
--dataset "${AGENT_SEMANTIC_DATASET}" \
6579
--policies "${AGENT_SEMANTIC_POLICIES}" \
66-
--switch-policy \
80+
"${SWITCH_ARGS[@]}" \
6781
--namespace "${ACTORDOCK_NAMESPACE}" \
6882
--min-workers "${AGENT_SEMANTIC_MIN_WORKERS}" \
6983
--max-inflight "${AGENT_SEMANTIC_INFLIGHT}" \

0 commit comments

Comments
 (0)