From ed502650f4b8c127d2bfad069774d696f15cbc74 Mon Sep 17 00:00:00 2001 From: simbiot-min Date: Tue, 29 Sep 2026 16:12:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=8D=D1=81=D0=BA=D0=B0=D0=BB=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20(min=E2=86=92hour=E2=86=92day)=20+=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=85=D0=B2=D0=BE=D1=81=D1=82=D0=B0?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=B2=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Часть 1 (часовой уровень, задача t-260929155421-9f7bff): - EscalationRaised/Acknowledged в ES-спину, проекция escalations - CLI: escalate / escalations / escalation-ack - уровни видят открытые эскалации в контексте; scheduler перезапускает критическую эскалацию сразу (не дожидаясь границы такта) Часть 2 (минутный, этот тик): - fix(cli): 'events -n N' показывал первые N событий вместо хвоста стора (README обещает хвост) — теперь хвост; --after N — от id N вперёд - + es.count_events() --- README.md | 14 ++++ __pycache__/cli.cpython-312.pyc | Bin 6567 -> 9788 bytes architecture.html | 2 + cli.py | 48 +++++++++++++- harness/__pycache__/__init__.cpython-312.pyc | Bin 371 -> 349 bytes harness/__pycache__/commands.cpython-312.pyc | Bin 5517 -> 9642 bytes harness/__pycache__/es.cpython-312.pyc | Bin 15379 -> 16797 bytes harness/__pycache__/levels.cpython-312.pyc | Bin 3863 -> 4846 bytes harness/__pycache__/runtime.cpython-312.pyc | Bin 6981 -> 6959 bytes harness/__pycache__/scheduler.cpython-312.pyc | Bin 14502 -> 17800 bytes harness/commands.py | 61 +++++++++++++++++- harness/es.py | 30 +++++++++ harness/levels.py | 16 ++++- harness/scheduler.py | 42 ++++++++++++ 14 files changed, 206 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3b3a308..5e1b597 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Strands Harness SDK как исполнительное ядро, модели `CycleTicked`, `TickAbsorbed`, `ReflexObserved`, `DigestWritten`, `TaskSubmitted`, `TaskClaimed`, `TaskCompleted`, `TaskFailed`, `TaskPromoted`, +`EscalationRaised`, `EscalationAcknowledged`, `BudgetSpent`, `CycleError`, `HarnessInit`, `PolicySet` — append-only, проекции пересобираются replay'ем, стейт = производное от событий. @@ -43,9 +44,22 @@ Strands Harness SDK как исполнительное ядро, модели ./venv/bin/python cli.py events -n 50 ./venv/bin/python cli.py requeue -r "причина" ./venv/bin/python cli.py promote -r "вне класса уровня" # S→M→L +./venv/bin/python cli.py escalate "причина" -f min -c "контекст" -s critical +./venv/bin/python cli.py escalations --open # открытые эскалации +./venv/bin/python cli.py escalation-ack -n "что сделал" # закрыть ./venv/bin/python cli.py tg-reply "текст" # ответ на входящее TG ``` +## Эскалации + +Важное с нижних уровней поднимается наверх событием `EscalationRaised` +(min→hour→day; day — вершина, эскалировать некуда). Открытые эскалации +инжектятся в контекст верхних уровней (`build_context`) в приоритетной секции +— нижний уровень не тонет в чужих проблемах, верхний видит выжимку, а не сырь. +Уровень-адресат закрывает эскалацию `escalation-ack` (событие +`EscalationAcknowledged`). Падение LLM-тика (`CycleError`) эскалируется +автоматически — scheduler поднимает warn-эскалацию на уровень выше. + ## Запуск ```bash diff --git a/__pycache__/cli.cpython-312.pyc b/__pycache__/cli.cpython-312.pyc index 0c609660f83fd73c5d8f9de9f22edc246d7efeb9..e409f331a81f9ccaa49bda0b2fd9295ee6499124 100644 GIT binary patch delta 3973 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 Gk@N=v0t+Aj diff --git a/architecture.html b/architecture.html index fb5323e..5907b1a 100644 --- a/architecture.html +++ b/architecture.html @@ -322,6 +322,8 @@
  • PolicyChanged {budgets, mandates}
  • BudgetSpent {level, tokens, $}
  • +
  • EscalationRaised {from_level, to_level, reason, context, severity} — важное с нижнего уровня наверх
  • +
  • EscalationAcknowledged {level, note} — верхний уровень принял/отработал
  • LevelPaused / LevelResumed
diff --git a/cli.py b/cli.py index 95666de..1191a3e 100644 --- a/cli.py +++ b/cli.py @@ -35,12 +35,29 @@ def main(): p_tgr.add_argument("msg_id", help="id входящего (m-NNN)") p_tgr.add_argument("text") + p_esc = sub.add_parser("escalate", help="эскалация важного на уровень выше (min→hour→day)") + p_esc.add_argument("reason") + p_esc.add_argument("-f", "--from-level", default="min", choices=["min", "hour"], + help="кто эскалирует") + p_esc.add_argument("-c", "--context", default="", help="контекст для верхнего уровня") + p_esc.add_argument("-s", "--severity", default="warn", choices=["info", "warn", "critical"]) + p_esc.add_argument("-t", "--task-id", default=None, help="связанная задача") + + p_escl = sub.add_parser("escalations", help="список эскалаций") + p_escl.add_argument("--open", action="store_true", help="только открытые") + + p_ack = sub.add_parser("escalation-ack", help="закрыть эскалацию (принята уровнем выше)") + p_ack.add_argument("esc_id") + p_ack.add_argument("-l", "--level", default="hour", help="кто принял") + p_ack.add_argument("-n", "--note", default="", help="что сделали с эскалацией") + sub.add_parser("status", help="состояние харнеса") sub.add_parser("tasks", help="доска задач") - p_ev = sub.add_parser("events", help="хвост стора") + p_ev = sub.add_parser("events", help="хвост стора (последние N событий)") p_ev.add_argument("-n", type=int, default=15) - p_ev.add_argument("--after", type=int, default=0) + p_ev.add_argument("--after", type=int, default=None, + help="вывести события ПОСЛЕ этого id (с головы, по возрастанию)") p_init = sub.add_parser("init", help="инициализация стора") p_init.add_argument("--identity", default="Simbiot Harness — ритмичный агент кооператива") @@ -70,6 +87,25 @@ def main(): from harness import tg_inbox mid = tg_inbox.send_reply(args.msg_id, args.text) print(f"OK: ответ на {mid} отправлен") + elif args.cmd == "escalate": + es.init_db() + eid, to_level = commands.raise_escalation( + args.from_level, args.reason, args.context, args.severity, args.task_id) + print(f"OK: эскалация {eid} → {to_level} ({args.severity})") + elif args.cmd == "escalations": + es.init_db() + rows = commands.list_escalations(only_open=args.open) + if not rows: + print("(эскалаций нет)") + for e in rows: + ctx = f" | {e.get('context','')[:60]}" if e.get("context") else "" + print(f"{e['id']} [{e.get('severity','info'):8}] {e.get('status','?'):5} " + f"{e.get('from_level')} → {e.get('to_level')} " + f"{e.get('reason','')[:70]}{ctx}") + elif args.cmd == "escalation-ack": + es.init_db() + eid = commands.acknowledge_escalation(args.esc_id, args.level, args.note) + print(f"OK: эскалация {eid} закрыта ({args.level})") elif args.cmd == "status": es.init_db() print(json.dumps(commands.status(), ensure_ascii=False, indent=1)) @@ -79,7 +115,13 @@ def main(): print(f"{t['id']} [{t['size']:2}] {t['status']:7} {t['title'][:70]}") elif args.cmd == "events": es.init_db() - for ev in es.get_events(args.after, args.n): + if args.after is None: + # хвост стора: последние N по возрастанию id + total = es.count_events() + evs = es.get_events(max(0, total - args.n), args.n) + else: + evs = es.get_events(args.after, args.n) + for ev in evs: d = ev["data"][:110] print(f"{ev['id']:4} {ev['timestamp'][11:19]} {ev['aggregate']:8} {ev['event_type']:16} {d}") diff --git a/harness/__pycache__/__init__.cpython-312.pyc b/harness/__pycache__/__init__.cpython-312.pyc index 13b6a4648638990cd123e8df3f8c8a7f4c852619..ee0f9de3e790701d18cdb5299f3ff665511a9918 100644 GIT binary patch delta 13 Ucmey&beCy^G$W(%WFtmL03fIXyZ`_I delta 35 qcmcc1^qFacG^4nteo=mYiGFcrZc=7`iEc(>QC@0svHoNkMn?eRiVQmd diff --git a/harness/__pycache__/commands.cpython-312.pyc b/harness/__pycache__/commands.cpython-312.pyc index f519eefa7961a8f380c3ea447b2372a45a51c5ce..d2eedc67cf535b50fc44972d38991354e491515f 100644 GIT binary patch delta 5147 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 delta 1532 zcmZuxOKjXk7`E4IujBRRu_3!`vb${aAUI7ELMjzOC{UC_sYuf%^bjE#XJWc!W5p!d z9epWF3d9CB2TYoD0%Mcnf0!;{a^ykM4LUv40-ZcCouqe1S?VGW0CumZoYL-?dQ#1) z(&GLGb?Xg}bMb0-$&>~U+pJD^*SoK(>V}vy3#ZeO$6uGZMZksja?_6@N*qI`e8Nl! zn_=uSjgDlN^2lxaK|7+HWr24r@tHZjl|elN^=z-zq5*N~6&gg2apZwvabg>p!2?}z zp9hMgSuI}ookV0WhXOiJYKA#ttmc{MM-?>nb!1mb5N=jW(TS zEM!vm147@DT7w3x6zjwe8R|nZ03(3Y%Q;*xgP9QOXFo_Unkv__TO(S~}fTsa4;{9+LeWOj=`{6zb zvg6|G(nk>6YX667eg@5+0+5;grikuxaSTs`$ozT~pDB%Yi+B-Dj@8+r6`K|p%hf_$ z7>m=$ZG{cLzRu2q{o~@D@`hS@uky(}R=`OgB_fS0*c>j6WVjqHj$dkFJ4%w!z~V3m ztKq|x;$U!g9t3B_-<1~!DzK}=bGUG}(QYp#=B@E#%BDh8tW%^PsF2hSvKq2$8R1#| zV)dck;*mW4fAY$~T}N9TZEvfI$4zA|E8ezx`;FI>+o^;j8KU$n#Jr&|C{D+mxKn#& z1Y6EV5%8$FTgt7JOietRq>6UfEwNa0)JwuU_E;M0CBCX%7>x~|I5cDza61pc53G>5 zByxjQbwP{|4g+2sENMD>Ni+v5-peSy3;-9`zsz^QW7xC0y$OC~3t z(-`J};~N0m3XZm2{m!o5wQJk6KNrj5nW1x2J%#s6*GnIq`uNP+rO)h+S)@PPi4 QQoU!0=SE!9RFhEu1yxpH_y7O^ diff --git a/harness/__pycache__/es.cpython-312.pyc b/harness/__pycache__/es.cpython-312.pyc index 804bae845739ab08292020caa35f5577eb1ae12d..88f0a082b20297ae70d25334c770280f81e6df1f 100644 GIT binary patch delta 3664 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 delta 2139 zcmbtVO>9(E6n=O9%KY?aIy0UB>9n@fK3btIZPB)L`om8tP_z&NC7}-7$2fF4Gu(Mi zTAYFj4YESQ-F9@T?eSg47GMA?gEcq<+9K4FEPR>EuxU8**Jos`JzkuI1~4 z4O{C$^8+a%esMrbR*gp_SvMscHKQ8K&SkYKnuse|l?PnSWG|1pLXmwCl!kxS5Ae41 z(C&EPa6NE)5$m1nR+G(CkWBoF>o4O0U@YY?H+)S@lVGy_eAE*(9s=_sKjS&AYd7<6 zJrgxK9W~6ggTF6l$mx0akc%Jl*6^MoCtviAac8}Q|LAq_X}wclQ}Wwuxl67tf5oN0SepCDUsSyBZ-F!p{-@uK>8i|BqEd---*49CjH19( zfm5P@p8~}~3V{eQb9b=GVTI+1D<{!*FIeZQ`D^_?t^~cLmR}0C=?@muxohdRJGvrw zc?+<&3$~rFhaB!Lv%v{r@z(`(B{gSJCJf8Ufm|c@<$NKM}SEcUD-hlvrK=Wx1ZQUKOn7oOI64?N*0= zAR+M%Yao!*-yyIjxJYqfgMv6%Ce{wcrE*)AEjbG(;f6P!g{bCYFm%UKo~66ycEhnV z=PY!XNAxuz78CVpER&BFDswa=0*uN#vL;Mu08y$Op>psx1HDs=mC$;)0i8 zN904?-ZE*LhIO-}Jl4{4d=yO0dKBIx+^Ss<-T?GS6&jyVAS6LkiqmRPT%GK49mtZ zz^vz;(YHu5|0Fs@&hy`*y_W<*6oAh#yNQ79W!n*MBkV+oB4B03LWP`AU&A?E8ADx$ zDr_%;0|Af8lPyEAjD@5!%%ijA>w~LMx3Nc2>lBlBJ$_} diff --git a/harness/__pycache__/levels.cpython-312.pyc b/harness/__pycache__/levels.cpython-312.pyc index b367113f16d5ce6fbc477fe7ca12876f2162d8d4..d99b55afa96bbc4212665a6eb6ff0c3f5067bcd4 100644 GIT binary patch delta 1162 zcma)5T}V@581@`xIQ5)BC$2R47!(GZQQeq|7u|FbT?SnRV`{Bzi>ZVmY&&KK{#6qi z1%VLKjc~Sg^33Vx4(ku1NEQ_`TKz|j)4!EY;af-*>16LLZ( zkd>u%IVU>s)ui`iA=O4Q%4}LC)9;i~mfIkGolH3|@cI;=GkXcrzz2&mXl%uCNFqZ% z=5m=7c#v15f}C+`)&z=*7-^sc3EY>~&WWAGf{?Q^$t|fswoIU1o`IV{XyG{7EkZ-# z&Ie;+U^o;U3XTj2A8lZ?EvGOHi9QjChY&n60O4T~iiJnQC>wtmj77%7k-=zBVRv^>z!LH71vYAzb7q%VwY&lsT!E^JX10_rvLxl*-v`DY$8NJ4TiPDs241dnu z=hgu5BA2ZIeH?6c3vg%Rm|=R+GRa#2#tNdVi13n!84pJzfneyK=nePxi9kg7_U#2+ z?3@Bi=j##Zmt+mgE~v18W34!_pdiPFJfaqvju&7@Mr9r2iNlzVbHSjv6CI6J%HH{< zx<5EJ+J;L*3((?Q-6b`nq)yj=g%odKLM$w&Vcu!>e%Xh=wfPtIdnE7K5i~QMFt{{8 z@+?~%(twrb-nA~s$sghG0PAbZcRGYS?_);b9ZI`?8idyni zHMRU`)f-&9?NwUKw$?azGf6N2VtX zlU4YhZNAPg!N}-6`Gde^M)}D{1eaK_3b`?T;9(FJ|02xA!}d{(fmi$nukZzF!yO_g zSZ?r%%!t^acA)A56O&Ln;|Dg7I$m~OwvPgn7YIp+G6FR}5EA>s%?8wJuEfRE!1lpw I@&zF|040DhGXMYp diff --git a/harness/__pycache__/runtime.cpython-312.pyc b/harness/__pycache__/runtime.cpython-312.pyc index c187164a594c485c63ee2159b89dc49339c0d44e..aca8eba4dbc57db2c132047320acd9159876aa9c 100644 GIT binary patch delta 14 VcmX?Vw%%-mE(@d3W)~JlaR4RM1Lyz% delta 36 rcmZ2)cGPTxE{nK^eo=mYiGFcrZc=7`iEc(>QC@0svHoT~7DjOZ^7Ra7 diff --git a/harness/__pycache__/scheduler.cpython-312.pyc b/harness/__pycache__/scheduler.cpython-312.pyc index e4508ee1f9199ccddc1451a0409930fd4a23ebd8..9e1c9cb2882469ca836b4d4af3b82cf87aab9f16 100644 GIT binary patch delta 5784 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 RWheo!`6J4}+6Ero_P?j)5HSD% delta 3067 zcmahLTWl29_1@W;eLuasUhlGg;WclA5hCIwXl7 zyMB>N0<{@l1Ca~lPl5Qg(Kkom_1+G?6Z|aD`B3iUaq%7bqn%OUTVBKDQ)aQOtCTmQ z76s786UdXbQgxc6t#&c54AQvxP&q^+;)#;!XhB&CW$VHLvsgHzRc6bRI_Fz~(J6Mh zGoW+S{d7k=ta%3ji*mzdYt_|!dbwz7sLLTXqKYYxO5?Tbo{LhT4G3ID5EIS*)QN3? zTE3!DovQM3-c)72VVA2Y)(^lyg*JGc7}88PbzUx3iUw?=vEsCDn3`eEPVyH(uvc96 zUk$_ovy^g`=_T-ZEa)T0gDB{yZ zus?W1y1G{;OCit<@eiiSQg|`M+^jJ}#>uI3XpyDNm}83&N0T;*_GHo(aaOVPy5%`) zO12%3Ee@u2Ot*q^I zT;xM>8Wy2c%12o{@=r&^FC*FIYq5I7kdLLfF2vaOx(i(yq^Q8%QRhb2_qavg^WF$ZSKGmS7|gpc7&q(w zFL(RZor}^Y%aUUmZ1%jTtmldnwhKahV}C+?I~7X@&%+5)c$Z?EF|}9xAoa6c0G#A_ z_&kIFJL9swv#W`B&R-zV zs@^mYV~~6RK=C>hjRwlOIc znWdOTv!p+M8R!dA+ev;vc$?pN&2M?i`3lQJmy7|&a_47e^a|s4vnf#W`W!!q4B4o1 z({i87mu7Xta>3lN}Qd943w$z8k@Si0#-)`^4am?&WzTa0ECa z?1XYqB77JDrquS`3i8>CRw|X9!N-7Z1;0L9!8v@G^D5^!LiiAZHUu&PY;g_~gxKey zhBi$<%suQ2fmt3r0SJmu0nKvH@M?L+v}?ywW{v!`Zh1|fFX$-JFT&l~j6L1LwnzFo zz2x|G8MKYscz2%^z|yuy+96kMx#v|$;?v?$?{$eaiQD}tnihL@kI{^n-#s~md-5v? z>@527lTb91I&<=91y8`7AFIv#}c714oQ1~a$d=lM!o7Y4AOn?QI*RlXpO59D66 zCFg(yRgB&*c;GKnX{yCaHS5Q5h3SNRf=m1?i!~7yzp}K)* zj{hKdoZnCg&Y=JWr1l^NEu~cVLi8!$ZLYY20#Ix;%Wv|3zH9 zIGl?|Hwa&T5h20kKZzn1h7Xp#B$|GAVCC$s;nm#gz{=?FBisMr-Ts$k#?gMh7EzS0 z=WF2&^=AfF&#ywe$8+Sql17;G9Z9(E_l%{EL|hMK3X2Ddd?ZC4L=?mmF2qxwBQ3Sq z3t_3lZV((i>urFcLl;-`te@MH?Im$&WDo5T3nRT>{XQ`NPbK5X`OjIX-v6!XJ7`}< z`%tW3jIk2TY6;_g^$KVE>i4#0Acy?Em2D~GukDOgtg zCW7xGup{{%VnRG0+xd+epq96k$J>T}7TWT4uIHKIl;npfXP3=5Vm|?3piwLse4L{3 zmGeay6V>Vr{}`B`)fNt=>2e%UD0j_fYCHlu~5O$EyyrXdMnl0%E_ int: + db = _conn() + n = db.execute("SELECT COUNT(*) AS c FROM events").fetchone()["c"] + db.close() + return n + + # ────────────────────────── ПРОЕКЦИИ ────────────────────────── def _default_model(): return { "tasks": {}, # task_id → {title, size, status, level, session_id, result_ref, ...} "digests": {}, # (level, period) → {summary_ref, text} + "escalations": {}, # esc_id → {from_level, to_level, reason, status: open|acked} "cycle_state": {}, # level → {busy, current_task, last_tick} "metrics": [], # [{level, ticks, absorbed, llm_ticks, tokens, cost}] "agents": {}, # level → {model, calls} @@ -135,6 +143,8 @@ def _apply_event(model: dict, ev: dict): etype, data = ev["event_type"], json.loads(ev["data"]) aid = ev["aggregate_id"] + if not isinstance(data, dict): + data = {} if etype == "TaskSubmitted": model["tasks"][aid] = { "id": aid, "title": data.get("title", ""), "size": data.get("size", "M"), @@ -174,6 +184,20 @@ def _apply_event(model: dict, ev: dict): keep = sorted(model["digests"].items(), key=lambda kv: kv[1].get("ts", ""), reverse=True)[:400] model["digests"] = dict(keep) + elif etype == "EscalationRaised": + model.setdefault("escalations", {})[aid] = { + **data, "status": "open", "ts": ev["timestamp"], + } + elif etype == "EscalationAcknowledged": + e = model.setdefault("escalations", {}).get(aid) + if e: + e["status"] = "acked" + e["acked_at"] = ev["timestamp"] + e["ack_level"] = data.get("level") + else: + model["escalations"][aid] = { + "status": "acked", "ack_level": data.get("level"), "ts": ev["timestamp"], + } elif etype == "CycleTicked": level = data.get("level", "?") cs = model["cycle_state"].setdefault(level, {"busy": False, "current_task": None, "ticks": 0}) @@ -214,6 +238,12 @@ def _apply_event(model: dict, ev: dict): m["ignored"] = True else: model["inbox"][aid] = {"ignored": True, "reason": data.get("reason"), "ts": ev["timestamp"]} + elif etype == "CycleError": + # счётчик ошибок по уровням — материал для эскалаций (демон/CLI) + cs = model.setdefault("cycle_state", {}).setdefault( + data.get("level", "?"), {"busy": False, "current_task": None, "ticks": 0}) + cs["errors"] = cs.get("errors", 0) + 1 + cs["last_error"] = (data.get("error") or "")[:300] # trace-события (InvocationStarted/ToolCalled/ToolResult) не материализуем — они для дебага diff --git a/harness/levels.py b/harness/levels.py index c1089a8..1d7aace 100644 --- a/harness/levels.py +++ b/harness/levels.py @@ -48,7 +48,11 @@ LEVELS = { "cli.py tg-reply \"текст\" (тон: кратко, по-русски, тёпло; можно " "выполнить просьбу инструментами и ответить результатом). Быстрые " "мелкие фиксы (S-задачи до 5 минут) тоже твои. Не растекайся. " - "Если просьба большая — подай её как задачу (cli.py task) и скажи это."), + "Если просьба большая — подай её как задачу (cli.py task) и скажи это. " + "Важное, что не по зубам уровню (падение тика, повторная ошибка, " + "требуется фикс с большим контекстом) — эскалируй наверх: " + "cli.py escalate \"причина\" -c \"контекст\" -s critical|warn|info; " + "эскалацию увидит часовой/дневной уровень в своём контексте."), can_shell=True, ), "hour": CycleLevel( @@ -56,14 +60,20 @@ LEVELS = { model="auto", effort="medium", ctx_tokens=32_000, task_classes=("S", "M"), mandate=("Ты — часовой поток харнеса Simbiot. Дайджест часа: что произошло, " "статусы фоновых задач, батч мелочей (S/M). Консолидируй минутные " - "события в выжимку — сырое наверх не поднимай."), + "события в выжимку — сырое наверх не поднимай. " + "Эскалации от нижних уровней (секция ЭСКАЛАЦИИ в контексте) — твоя " + "зона: разберись и закрой cli.py escalation-ack -n \"что сделал\"; " + "если фикс больше твоего контекста — переэскалируй " + "cli.py escalate \"причина\" -f hour -s critical."), can_shell=True, ), "day": CycleLevel( name="day", label="День · манифест", every=DAY, uses_llm=True, model="auto", effort="high", ctx_tokens=128_000, task_classes=("S", "M", "L"), mandate=("Ты — дневной цикл харнеса Simbiot. Манифест дня: план, выбор задач " - "L-класса, дневной отчёт. Думай глубоко, планируй на день вперёд."), + "L-класса, дневной отчёт. Думай глубоко, планируй на день вперёд. " + "Открытые эскалации (секция ЭСКАЛАЦИИ) — приоритет: это важное, " + "поднятое с нижних уровней; закрой cli.py escalation-ack ."), can_shell=True, ), } diff --git a/harness/scheduler.py b/harness/scheduler.py index dd73f2d..9445f93 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -16,6 +16,8 @@ from datetime import datetime, timezone, timedelta from . import es from .levels import LEVELS from . import runtime +from . import commands +from .commands import _ORDER # интервал демона POLL = 1.0 @@ -65,6 +67,11 @@ def find_task(level_cfg, model: dict): return None +def _lvl_rank(name: str) -> int: + """Ранг уровня в лестнице; неизвестный → ниже всех (эскалации ему не адресованы).""" + return _ORDER.index(name) if name in _ORDER else len(_ORDER) + + def build_context(level_cfg, model: dict, task=None): """Сборка контекста тика: мандат + память уровня + выжимки нижних + задача.""" parts = [level_cfg.mandate] @@ -89,6 +96,27 @@ def build_context(level_cfg, model: dict, task=None): for d in digests[:6]: parts.append(f"- [{d['level']}] {d['text'][:400]}") + # эскалации: открытые, адресованные этому уровню и выше — в приоритете + escs = [] + for e in (model.get("escalations") or {}).values(): + if not isinstance(e, dict) or e.get("status") != "open" or e.get("stub"): + continue + to_level = e.get("to_level", "day") + if to_level not in _ORDER or _lvl_rank(level_cfg.name) < _lvl_rank(to_level): + continue + escs.append(e) + if escs: + escs.sort(key=lambda e: ({'critical': 0, 'warn': 1, 'info': 2}.get(e.get("severity"), 3), + e.get("ts", "")), reverse=False) + lines = [] + for e in escs[:10]: + who = f"от {e.get('from_level')}" if e.get("from_level") else "" + lines.append(f"- [{e['id']}] ({e.get('severity','info')}) {who} → " + f"{e.get('to_level')}: {e.get('reason','')[:300]}" + + (f"\n контекст: {e.get('context','')[:400]}" if e.get("context") else "")) + parts.append("ЭСКАЛАЦИИ (важное с нижних уровней; отработай и закрой: " + "./venv/bin/python cli.py escalation-ack ):\n" + "\n".join(lines)) + # инбокс: входящие TG-сообщения, ждущие ответа (в приоритете) inbox = [m for m in (model.get("inbox") or {}).values() if not m.get("replied") and not m.get("ignored") and m.get("text")] @@ -184,6 +212,20 @@ def process_level(level_cfg, model: dict): "level": level_cfg.name, "error": str(e)[:1000], "trace": traceback.format_exc()[-2000:], }) + # воркер LLM-тика умер — наверх, чтобы верхний уровень узнал сразу: + # нижний сам себя не чинит (контекст мал), фикс идёт сверху. + try: + import secrets as _secrets + esc_id = f"e-{datetime.now(timezone.utc).strftime('%y%m%d%H%M%S')}-{_secrets.token_hex(3)}" + es.append_event("harness", esc_id, "EscalationRaised", { + "id": esc_id, "from_level": level_cfg.name, + "to_level": commands.level_above(level_cfg.name) or "day", + "reason": f"тик {level_cfg.name} упал: {str(e)[:200]}", + "context": traceback.format_exc()[-1500:], + "severity": "warn", "task_id": None, "source": "scheduler", + }) + except Exception: + pass # эскалация — best effort: не маскировать исходную ошибку return "error: " + str(e)[:120] finally: st["busy"] = False