Skip to content

Commit 091a5c7

Browse files
committed
runtime_cli: export per-agent eventline and add lens script
1 parent e049b64 commit 091a5c7

3 files changed

Lines changed: 570 additions & 1 deletion

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
param(
2+
[Parameter(Mandatory = $true)]
3+
[string]$Eventline,
4+
[string]$Out = "",
5+
[int]$FromStep = -1,
6+
[int]$ToStep = -1,
7+
[int]$MaxEvents = 600
8+
)
9+
10+
$ErrorActionPreference = "Stop"
11+
12+
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
13+
$script = Join-Path $repoRoot "scripts\\observe_agent_eventline.py"
14+
15+
if (!(Test-Path $script)) {
16+
throw "Missing script: $script"
17+
}
18+
19+
$argsList = @(
20+
$script,
21+
"--eventline", $Eventline,
22+
"--max-events", $MaxEvents
23+
)
24+
25+
if ($FromStep -ge 0) {
26+
$argsList += @("--from-step", $FromStep)
27+
}
28+
if ($ToStep -ge 0) {
29+
$argsList += @("--to-step", $ToStep)
30+
}
31+
if (-not [string]::IsNullOrWhiteSpace($Out)) {
32+
$argsList += @("--out", $Out)
33+
}
34+
35+
& python @argsList | Write-Host
36+
if ($LASTEXITCODE -ne 0) {
37+
throw ("observe_agent_eventline failed (exit={0})" -f $LASTEXITCODE)
38+
}
39+

scripts/observe_agent_eventline.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import json
6+
from dataclasses import dataclass
7+
from pathlib import Path
8+
from typing import Any, Iterable
9+
10+
11+
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
12+
rows: list[dict[str, Any]] = []
13+
with path.open("r", encoding="utf-8") as f:
14+
for line in f:
15+
s = line.strip()
16+
if not s:
17+
continue
18+
rows.append(json.loads(s))
19+
return rows
20+
21+
22+
def _fmt(v: Any) -> str:
23+
if v is None:
24+
return "n/a"
25+
return str(v)
26+
27+
28+
def _fmt_float(x: Any, digits: int = 2) -> str:
29+
try:
30+
v = float(x)
31+
except Exception:
32+
return "n/a"
33+
return f"{v:.{digits}f}"
34+
35+
36+
@dataclass(frozen=True)
37+
class Event:
38+
step: int
39+
type: str
40+
payload: dict[str, Any]
41+
42+
43+
def _iter_events(rows: Iterable[dict[str, Any]]) -> tuple[dict[str, Any], list[Event]]:
44+
meta: dict[str, Any] | None = None
45+
events: list[Event] = []
46+
for r in rows:
47+
kind = r.get("kind")
48+
if kind == "runtime_eventline_meta" and meta is None:
49+
meta = r
50+
continue
51+
if kind == "runtime_eventline_event":
52+
events.append(
53+
Event(
54+
step=int(r.get("step", 0) or 0),
55+
type=str(r.get("type", "")),
56+
payload=dict(r.get("payload", {}) or {}),
57+
)
58+
)
59+
if meta is None:
60+
meta = {}
61+
events.sort(key=lambda e: e.step)
62+
return meta, events
63+
64+
65+
def _render_event(e: Event) -> list[str]:
66+
p = e.payload
67+
t = e.type
68+
if t == "initial_state":
69+
needs = p.get("needs", {}) or {}
70+
parts = []
71+
for name, nv in needs.items():
72+
parts.append(f"{name}={_fmt_float(nv.get('value'))}{'(!)' if nv.get('critical') else ''}")
73+
return [f"- t={e.step} 初始状态:map={_fmt(p.get('mapId'))} target={_fmt(p.get('plannerTarget'))} action={_fmt(p.get('action'))} needs: " + ", ".join(parts)]
74+
75+
if t == "map_change":
76+
return [f"- t={e.step} 跨图:{_fmt(p.get('from'))}{_fmt(p.get('to'))}"]
77+
78+
if t == "planner_target_change":
79+
extra = " (agentTarget)" if p.get("toIsAgentTarget") else ""
80+
return [
81+
f"- t={e.step} 目标切换:{_fmt(p.get('from'))}{_fmt(p.get('to'))}{extra} travel={_fmt_float(p.get('travelCost'))} score={_fmt_float(p.get('score'))}"
82+
]
83+
84+
if t == "action_change":
85+
base = f"- t={e.step} 动作切换:{_fmt(p.get('from'))}{_fmt(p.get('to'))} q={_fmt(p.get('queueLength'))}"
86+
to = p.get("to", "")
87+
if to == "SocializeWithAgent":
88+
return [base + f" partner={_fmt(p.get('targetEntityId'))}"]
89+
if to in ("ConsumeResource", "TakeResource", "ProduceResource", "MoveToInteraction"):
90+
return [base + f" target={_fmt(p.get('target'))} res={_fmt(p.get('resourceType'))} amount={_fmt(p.get('amount'))}"]
91+
return [base]
92+
93+
if t == "need_critical_transition":
94+
return [f"- t={e.step} 临界变化:{_fmt(p.get('need'))} critical {p.get('from')}{p.get('to')} value={_fmt_float(p.get('value'))}"]
95+
96+
if t == "need_delta":
97+
dv = p.get("delta")
98+
sign = ""
99+
try:
100+
sign = "↑" if float(dv) > 0 else "↓"
101+
except Exception:
102+
sign = ""
103+
return [
104+
f"- t={e.step} 需求波动:{_fmt(p.get('need'))} {sign} delta={_fmt_float(dv)} ({_fmt_float(p.get('from'))}{_fmt_float(p.get('to'))})"
105+
]
106+
107+
if t == "resource_attempt":
108+
ok = (p.get("failureReason") in (None, "")) and int(p.get("obtainedUnits", 0) or 0) > 0
109+
status = "成功" if ok else f"失败({p.get('failureReason') or 'n/a'})"
110+
return [
111+
f"- t={e.step} 资源尝试:{_fmt(p.get('action'))} {status} {p.get('resourceType')}@{_fmt(p.get('interactionId'))} want={_fmt(p.get('wantedUnits'))} got={_fmt(p.get('obtainedUnits'))} recoveryPlanned={_fmt(p.get('recoveryPlanned'))}"
112+
]
113+
114+
if t == "workshop_attempt":
115+
ok = (p.get("failureReason") in (None, "")) and int(p.get("producedUnits", 0) or 0) > 0
116+
status = "成功" if ok else f"失败({p.get('failureReason') or 'n/a'})"
117+
return [
118+
f"- t={e.step} 生产尝试:{status} out={_fmt(p.get('outputType'))} workshop={_fmt(p.get('interactionId'))} wantUnits={_fmt(p.get('wantedUnits'))} got={_fmt(p.get('producedUnits'))}"
119+
]
120+
121+
if t == "social_attempt_start":
122+
return [f"- t={e.step} 社交开始:partner={_fmt(p.get('partnerEntityId'))} intendedRelief={_fmt_float(p.get('intendedRelief'))} social={_fmt_float(p.get('socialValue'))}"]
123+
124+
if t == "social_attempt_end":
125+
return [
126+
f"- t={e.step} 社交结束:partner={_fmt(p.get('partnerEntityId'))} inferredSuccess={_fmt(p.get('inferredSuccess'))} socialDelta={_fmt_float(p.get('socialDelta'))} intendedRelief={_fmt_float(p.get('intendedRelief'))}"
127+
]
128+
129+
return [f"- t={e.step} {t}: {json.dumps(p, ensure_ascii=False)}"]
130+
131+
132+
def render_markdown(eventline_path: Path, from_step: int | None, to_step: int | None, max_events: int) -> str:
133+
rows = _read_jsonl(eventline_path)
134+
meta, events = _iter_events(rows)
135+
136+
if from_step is not None:
137+
events = [e for e in events if e.step >= from_step]
138+
if to_step is not None:
139+
events = [e for e in events if e.step <= to_step]
140+
141+
if max_events > 0 and len(events) > max_events:
142+
events = events[:max_events]
143+
144+
counts: dict[str, int] = {}
145+
for e in events:
146+
counts[e.type] = counts.get(e.type, 0) + 1
147+
148+
social_starts = sum(1 for e in events if e.type == "social_attempt_start")
149+
social_ends = [e for e in events if e.type == "social_attempt_end"]
150+
social_success = sum(1 for e in social_ends if bool(e.payload.get("inferredSuccess")))
151+
152+
resource_attempts = [e for e in events if e.type == "resource_attempt"]
153+
resource_fail = 0
154+
for e in resource_attempts:
155+
p = e.payload
156+
ok = (p.get("failureReason") in (None, "")) and int(p.get("obtainedUnits", 0) or 0) > 0
157+
if not ok:
158+
resource_fail += 1
159+
160+
out: list[str] = []
161+
out.append("# Agent Lens(对象演化视角)\n")
162+
out.append("## 元信息\n")
163+
out.append(f"- eventline: `{eventline_path.as_posix()}`")
164+
out.append(f"- agentEntityId: `{_fmt(meta.get('agentEntityId'))}` agentIndexSorted: `{_fmt(meta.get('agentIndexSortedByEntityId'))}`")
165+
out.append(f"- worldSeed: `{_fmt(meta.get('worldSeed'))}` worldgenConfig: `{_fmt(meta.get('worldgenConfig'))}`")
166+
out.append(f"- stepsRequested: `{_fmt(meta.get('stepsRequested'))}` telemetrySchemaVersion: `{_fmt(meta.get('telemetrySchemaVersion'))}`\n")
167+
168+
out.append("## 摘要\n")
169+
out.append(f"- 事件总数: `{len(events)}`(截断上限 maxEvents={max_events})")
170+
out.append(f"- 资源尝试: `{len(resource_attempts)}` 失败: `{resource_fail}`")
171+
out.append(f"- 社交尝试: start `{social_starts}` end `{len(social_ends)}` inferredSuccess `{social_success}`\n")
172+
173+
out.append("## 事件计数(按类型)\n")
174+
for k in sorted(counts.keys()):
175+
out.append(f"- `{k}`: `{counts[k]}`")
176+
out.append("")
177+
178+
out.append("## 时间线(按 step)\n")
179+
for e in events:
180+
out.extend(_render_event(e))
181+
out.append("")
182+
183+
return "\n".join(out)
184+
185+
186+
def main() -> int:
187+
ap = argparse.ArgumentParser()
188+
ap.add_argument("--eventline", type=Path, required=True, help="Path to runtime_eventline JSONL")
189+
ap.add_argument("--out", type=Path, default=None, help="Output markdown path")
190+
ap.add_argument("--from-step", type=int, default=None)
191+
ap.add_argument("--to-step", type=int, default=None)
192+
ap.add_argument("--max-events", type=int, default=600)
193+
args = ap.parse_args()
194+
195+
md = render_markdown(args.eventline, args.from_step, args.to_step, args.max_events)
196+
if args.out is None:
197+
print(md)
198+
return 0
199+
args.out.parent.mkdir(parents=True, exist_ok=True)
200+
args.out.write_text(md, encoding="utf-8")
201+
print(f"Wrote: {args.out.as_posix()}")
202+
return 0
203+
204+
205+
if __name__ == "__main__":
206+
raise SystemExit(main())
207+

0 commit comments

Comments
 (0)