@maudecode/talaria-web 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +4 -4
- package/dist/api/automation-router.d.ts +102 -0
- package/dist/api/automation-router.js +2 -0
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +589 -52
- package/dist/api/chat-router.js +111 -121
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/router.d.ts +2 -1
- package/dist/api/router.js +3 -1
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +1913 -299
- package/dist/api/sessions-router.js +35 -24
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +35 -8
- package/dist/api/settings-router.js +14 -10
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.js +1 -1
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/app.d.ts +2715 -418
- package/dist/config/agent-config.js +20 -15
- package/dist/config/agent-config.js.map +1 -1
- package/dist/http/context.d.ts +4 -2
- package/dist/http/context.js.map +1 -1
- package/dist/onboarding.d.ts +1 -1
- package/dist/onboarding.js +2 -1
- package/dist/onboarding.js.map +1 -1
- package/dist/providers/catalog.d.ts +11 -0
- package/dist/providers/catalog.js +32 -1
- package/dist/providers/catalog.js.map +1 -1
- package/dist/runtime.js +54 -16
- package/dist/runtime.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +137 -0
- package/dist/sessions/background-tasks.js +349 -0
- package/dist/sessions/background-tasks.js.map +1 -0
- package/dist/sessions/background-updates.d.ts +56 -0
- package/dist/sessions/background-updates.js +152 -0
- package/dist/sessions/background-updates.js.map +1 -0
- package/dist/sessions/cli-sessions.d.ts +6 -0
- package/dist/sessions/cli-sessions.js +20 -7
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +17 -1
- package/dist/sessions/completions.js +83 -12
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +33 -0
- package/dist/sessions/compress.js +66 -0
- package/dist/sessions/compress.js.map +1 -0
- package/dist/sessions/list.d.ts +14 -0
- package/dist/sessions/list.js +89 -16
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +12 -1
- package/dist/sessions/merge.js +64 -7
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/relay.d.ts +7 -1
- package/dist/sessions/relay.js +17 -4
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +61 -3
- package/dist/sessions/service.js +243 -15
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +16 -0
- package/dist/sessions/session.js +23 -2
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/source-kind.d.ts +22 -0
- package/dist/sessions/source-kind.js +54 -0
- package/dist/sessions/source-kind.js.map +1 -0
- package/dist/sessions/state-db.d.ts +10 -1
- package/dist/sessions/state-db.js +48 -17
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.d.ts +2 -0
- package/dist/sessions/store.js +23 -4
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +3 -0
- package/dist/sessions/streams.js +0 -5
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +4 -0
- package/dist/sessions/titles.js +16 -0
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +1 -0
- package/dist/sessions/turn-context.js +3 -0
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +66 -6
- package/dist/sessions/turn.js +304 -61
- package/dist/sessions/turn.js.map +1 -1
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +4 -0
- package/dist/settings.js.map +1 -1
- package/dist/tools/crons.d.ts +34 -3
- package/dist/tools/crons.js +271 -31
- package/dist/tools/crons.js.map +1 -1
- package/package.json +2 -2
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/methods/chat.py +233 -6
- package/sidecar/talaria_sidecar/methods/cron.py +22 -14
- package/sidecar/talaria_sidecar/methods/process.py +318 -0
- package/static/dist/FILES.txt +120 -119
- package/static/dist/assets/AppShell-orat1s43.js +1 -0
- package/static/dist/assets/ChatPage-ojhka3hy.js +22 -0
- package/static/dist/assets/HubRoute-gxgw1t33.js +2 -0
- package/static/dist/assets/{Markdown-emii55o0.js → Markdown-dy8t4dy7.js} +7 -7
- package/static/dist/assets/{RightPanel-o73y98dn.js → RightPanel-c66ani1s.js} +1 -1
- package/static/dist/assets/Select-fswf66up.js +1 -0
- package/static/dist/assets/SessionListPanel-goh5mtt8.js +1 -0
- package/static/dist/assets/{SettingsLayout-lnn2wo2s.js → SettingsLayout-n5d4wq4m.js} +1 -1
- package/static/dist/assets/{States-e5hv6gdh.js → States-kxyu50at.js} +1 -1
- package/static/dist/assets/{Toaster-c4jq1i26.js → Toaster-6zmbcqgg.js} +1 -1
- package/static/dist/assets/_app-fowyruiu.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +1 -0
- package/static/dist/assets/_app.index-c9zv4y7w.js +1 -0
- package/static/dist/assets/_app.insights-jf12nofo.js +1 -0
- package/static/dist/assets/_app.kanban-lri5a4yu.js +1 -0
- package/static/dist/assets/_app.logs-md1p0zqo.js +1 -0
- package/static/dist/assets/_app.memory-bzvf2e28.js +1 -0
- package/static/dist/assets/_app.profiles-jpg7zmbn.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-m6nx9g8t.js → _app.session._sessionId-gyoh5ylx.js} +1 -1
- package/static/dist/assets/_app.settings-mpuwlx4j.js +1 -0
- package/static/dist/assets/_app.settings._section-knz8rwoj.js +1 -0
- package/static/dist/assets/_app.skills-fwu1dkir.js +1 -0
- package/static/dist/assets/_app.tasks-h9gphtl3.js +2 -0
- package/static/dist/assets/_app.todos-hd1dnvrh.js +1 -0
- package/static/dist/assets/_app.workspaces-l8toyn5f.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-kejgxpil.js → abnfDiagram-VCTEODGH-cjqletra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-hqnf84fc.js → architectureDiagram-5GKGNRK7-k5a7v36n.js} +1 -1
- package/static/dist/assets/back-e4m6nsnm.js +1 -0
- package/static/dist/assets/{blockDiagram-I7D4REHJ-l1466a7j.js → blockDiagram-I7D4REHJ-b195z021.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-gg3aq3sr.js → c4Diagram-7LVT6UL2-olu212k1.js} +1 -1
- package/static/dist/assets/channel-gozy3dp3.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-oalgz5l8.js → chunk-4HAMMTFA-b8o1nufs.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-babmzhdg.js → chunk-75Z2AOVW-l1seddto.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lagu350c.js → chunk-DU6HZSFF-lo5zg15v.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-gjh2tqif.js → chunk-F27PBJKO-dsdzrp16.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-ipdgja5c.js → chunk-GMAD6QVW-f3fxqq8b.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-j8wkn1at.js → chunk-GVQU2GXP-h8egxtf6.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-k00laqyk.js → chunk-IMKFNOWR-iolwd8s0.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-fju6xqj4.js → chunk-L3NEJ4N5-b88bbtwi.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-bi6widqt.js → chunk-OSK3NFVY-e3e7nr6s.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-czr4tebv.js → chunk-P2QGCYS3-mdc46rt1.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-ktdhn67w.js → chunk-POPQ4Y6H-kqlllrrm.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-ii4dsu0d.js → chunk-PWAF6VOD-he585k2x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-fpwk3xxp.js → chunk-SHT3W25Y-gh94y9a4.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-k0jrvhei.js → chunk-SVP7TREG-euxp9y7e.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-nkqv9ot0.js → chunk-TICWLB2K-c8cxes1q.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +1 -0
- package/static/dist/assets/{copied-gpchexqu.js → copied-kq0u3ht7.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-lqxt8rs3.js → cynefin-OW5HDTMX-lxd44ux4.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-j9eqovf5.js → cynefinDiagram-5FMLGOSQ-njpiwat8.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-m9agobjm.js → dagre-GXQ25YYZ-nvgy6yju.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-itodc50c.js → diagram-S7CK7UJ4-g98nyv72.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-n1h2t4ah.js → diagram-UQ7AKVKN-ovgm6lvv.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-lvtvm9bj.js → diagram-VSXAHHWV-pb2bn2e4.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-cgas58sy.js → diagram-VX7I27RA-6j4mvfqo.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-d0aeps1q.js → diagram-Z3DM3KII-dqmb982r.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-md345fyq.js → ebnfDiagram-PWID7BFC-ozbpzb6d.js} +1 -1
- package/static/dist/assets/{endpoints-e58saih3.js → endpoints-k8agnro8.js} +8 -8
- package/static/dist/assets/{erDiagram-RLTQ6QDP-i4rua63b.js → erDiagram-RLTQ6QDP-hl7m8vua.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-geqx7zgl.js → ganttDiagram-EL5Y4UJY-ompixqmh.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-leoq0z8t.js → gitGraph-4MIJSDKK-b6vxwp9q.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-llrlxfha.js → gitGraphDiagram-WWUBYQGX-k98f57on.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-kq2cnw6q.js → highlighted-body-KPVGNVTW-cd3ff6ge.js} +1 -1
- package/static/dist/assets/index-nfy0ezs6.css +2 -0
- package/static/dist/assets/{index-f9bzglkn.js → index-nqed3oum.js} +3 -3
- package/static/dist/assets/{info-A6RAGUB7-lkly4xj6.js → info-A6RAGUB7-hjcwjami.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-oo7kdyte.js → infoDiagram-27XIBGKW-fm06rj36.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-hq6jf374.js → ishikawaDiagram-5VMMS53U-bxak4mwu.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-is03ntb0.js → journeyDiagram-3NMN7TZE-khmihclr.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-jmerizqb.js → kanban-definition-UXKFOSKX-h1r32wdx.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-b1h2ykmb.js → lazyRouteComponent-b2eg0fws.js} +1 -1
- package/static/dist/assets/{line-laaa8h99.js → line-friyjof3.js} +1 -1
- package/static/dist/assets/{link-d8f0788e.js → link-m90ahx3n.js} +1 -1
- package/static/dist/assets/login-bw7m2fzo.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-ix4junux.js → mermaid-parser.core-f1gvpwrt.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-o0e0adl1.js → mindmap-definition-YA3MSWOX-mt2oxwue.js} +1 -1
- package/static/dist/assets/{onboarding-lx0d6b38.js → onboarding-ret37j1e.js} +1 -1
- package/static/dist/assets/{packet-AYTQ26CC-kl8pwsyg.js → packet-AYTQ26CC-jik4ixlf.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-bbqn9jo4.js → pegDiagram-XKGWAZYB-erxlv6cc.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-cpyo8bkr.js → pie-WAS4IAKB-mnkoldfl.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-pelug34f.js → pieDiagram-E7YTZNPT-i7lwrada.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-jds4jmr5.js → quadrantDiagram-AXDQQJYC-fr8ejdoy.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-e02oxnvd.js → radar-RG4KPBEZ-otarjte2.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-kz3delwo.js → railroad-74A4TZTK-lw362oto.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-jgroj2ew.js → railroadDiagram-O6MQD6OU-esyn75pw.js} +1 -1
- package/static/dist/assets/{refresh-cw-k7yxjdn3.js → refresh-cw-k66keho6.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-m4az5e0j.js → requirementDiagram-BXWQKSXE-cyhi5u29.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-vlryqiv3.js → sankeyDiagram-P5KCCOFB-jljpbo0h.js} +1 -1
- package/static/dist/assets/{saved-brdfjjnw.js → saved-do49si3v.js} +1 -1
- package/static/dist/assets/{search-gt2ueyvg.js → search-j5ki6a9o.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fr8hrpql.js → sequenceDiagram-WJ2MYXX4-fz0dhlu7.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-l0zodj0b.js → settings_section_extensions_title-b7lojn0o.js} +1 -1
- package/static/dist/assets/share._token-kg8w647b.js +2 -0
- package/static/dist/assets/{sliders-horizontal-gcgscv8r.js → sliders-horizontal-c4wya4dy.js} +1 -1
- package/static/dist/assets/{stateDiagram-D77RDMKH-bg548iyr.js → stateDiagram-D77RDMKH-kdnithdx.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-hk5z3efh.js → swimlanes-42K2YHIH-esd6gmz3.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +8 -0
- package/static/dist/assets/tabBuild-bjmyvmn5.js +56 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-olvknr62.js → timeline-definition-24CTP7MA-g0qygccv.js} +1 -1
- package/static/dist/assets/todoStore-kjk5l0sy.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-ovvjsxtf.js → treeView-Q6P3EWNA-ndchzkuu.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-ggkipj59.js → treemap-WGGIJYW6-hwjxj0zz.js} +1 -1
- package/static/dist/assets/{untitled-c8ydw0bu.js → untitled-mbr6tuhu.js} +1 -1
- package/static/dist/assets/{useForm-kd759sjk.js → useForm-ocxu460f.js} +1 -1
- package/static/dist/assets/useLocale-fzy5jhg6.js +1 -0
- package/static/dist/assets/{useParams-katwvzil.js → useParams-lo9u33d1.js} +1 -1
- package/static/dist/assets/{useSearch-c70sgau1.js → useSearch-lq1mr090.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-hzld7zt7.js → vennDiagram-4TSXK5OY-gubovd1r.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-hxkeoms8.js → wardley-WFR3VGLG-ir3om65j.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ljh90mb1.js → wardleyDiagram-VM6X3IG4-ce0or1ie.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mnuaes03.js → xychartDiagram-S5SC5T6Z-dexylcjg.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-haqi9qzu.js +0 -1
- package/static/dist/assets/ChatPage-hjyu2g64.js +0 -22
- package/static/dist/assets/HubRoute-gkd8o1vg.js +0 -2
- package/static/dist/assets/Select-jld3yeni.js +0 -1
- package/static/dist/assets/SessionListPanel-d3qx4ric.js +0 -1
- package/static/dist/assets/_app-bnip2pjg.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-egptzrfy.js +0 -1
- package/static/dist/assets/_app.index-vdz13ul5.js +0 -1
- package/static/dist/assets/_app.insights-lgjshsba.js +0 -1
- package/static/dist/assets/_app.kanban-c18cjrpz.js +0 -1
- package/static/dist/assets/_app.logs-ezhojgeg.js +0 -1
- package/static/dist/assets/_app.memory-i00paisv.js +0 -1
- package/static/dist/assets/_app.profiles-kd02bwjx.js +0 -1
- package/static/dist/assets/_app.settings-giaudkuk.js +0 -1
- package/static/dist/assets/_app.settings._section-k4qtlexk.js +0 -1
- package/static/dist/assets/_app.skills-bby2uvl8.js +0 -1
- package/static/dist/assets/_app.tasks-1kz9bosm.js +0 -2
- package/static/dist/assets/_app.todos-izjdbkm2.js +0 -1
- package/static/dist/assets/_app.workspaces-lfh2kw7s.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-b4u9z1w3.js +0 -1
- package/static/dist/assets/channel-o77ioktg.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-f5rvod26.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-f5rvod26.js +0 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-bn3fjsaq.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-li70d120.js +0 -1
- package/static/dist/assets/index-gkh7kum4.css +0 -2
- package/static/dist/assets/login-llkohrcg.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-id0ffk2r.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-l13ped2z.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-c7rg3g4g.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-od0b8niq.js +0 -1
- package/static/dist/assets/share._token-oyjv046r.js +0 -2
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-j5v40qih.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-grzefo9c.js +0 -8
- package/static/dist/assets/tabBuild-idliyw9f.js +0 -56
- package/static/dist/assets/todoStore-j4c2xq0b.js +0 -1
- package/static/dist/assets/useLocale-hds4xmja.js +0 -1
|
@@ -257,13 +257,86 @@ def _clarify_timeout(params: dict) -> int:
|
|
|
257
257
|
return 3600
|
|
258
258
|
|
|
259
259
|
|
|
260
|
+
def _steer_slot_lock(agent):
|
|
261
|
+
"""The Agent's own pending-steer lock (a plain ``Lock``), so a slot read or rewrite is atomic against its drains."""
|
|
262
|
+
lock = agent.__dict__.get("_pending_steer_lock") if hasattr(agent, "__dict__") else None
|
|
263
|
+
return contextlib.nullcontext() if lock is None else lock
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _steer_slot(agent) -> str:
|
|
267
|
+
"""The Agent's not-yet-applied steer text; call under ``_steer_slot_lock``."""
|
|
268
|
+
return str(getattr(agent, "_pending_steer", "") or "")
|
|
269
|
+
|
|
270
|
+
|
|
260
271
|
def _agent_pending_steer_text(agent) -> str:
|
|
261
272
|
"""Predecessor ``_agent_pending_steer_text``: the Agent's not-yet-applied steer text."""
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
with _steer_slot_lock(agent):
|
|
274
|
+
return _steer_slot(agent)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _slot_without(slot: str, pending: list[str], index: int) -> str | None:
|
|
278
|
+
"""``slot`` without the steer ``pending[index]`` while the Agent still holds it, else None (it already took it).
|
|
279
|
+
|
|
280
|
+
The Agent keeps every not-yet-applied steer in one newline-joined slot, so the slot is the tail of ``pending``
|
|
281
|
+
(the server's steers, oldest first), possibly behind text another surface queued: the server's
|
|
282
|
+
``pendingSteerSuffixStart`` rule. The longest tail that still holds the steer wins."""
|
|
283
|
+
for start in range(index + 1):
|
|
284
|
+
tail = "\n".join(pending[start:])
|
|
285
|
+
if slot == tail or slot.endswith("\n" + tail):
|
|
286
|
+
head = slot[: len(slot) - len(tail)].rstrip("\n")
|
|
287
|
+
return "\n".join(part for part in [head, *pending[start:index], *pending[index + 1:]] if part)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def withdraw_steer(agent, pending: list[str], index: int) -> bool:
|
|
292
|
+
"""TAL-424: take a not-yet-applied steer back out of the Agent's slot, keeping the others in order. False when the
|
|
293
|
+
Agent already took it; the slot is then left as it was, so no text is ever lost."""
|
|
294
|
+
with _steer_slot_lock(agent):
|
|
295
|
+
rest = _slot_without(_steer_slot(agent), pending, index)
|
|
296
|
+
if rest is None:
|
|
297
|
+
return False
|
|
298
|
+
agent._pending_steer = rest or None
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def steer_now(agent, pending: list[str], index: int) -> dict:
|
|
303
|
+
"""TAL-424: deliver a pending steer now with the Agent's ``redirect``. During a model request that request restarts
|
|
304
|
+
with it (``delivery: redirect``); during tools it goes back on the slot and the tools yield (``delivery: steer``).
|
|
305
|
+
With no live request the steer stays pending: back in its place, or last when the slot changed meanwhile."""
|
|
306
|
+
text = pending[index]
|
|
307
|
+
with _steer_slot_lock(agent):
|
|
308
|
+
before = _steer_slot(agent)
|
|
309
|
+
rest = _slot_without(before, pending, index)
|
|
310
|
+
if rest is None:
|
|
311
|
+
return {"redirected": False, "withdrawn": False}
|
|
312
|
+
agent._pending_steer = rest or None
|
|
313
|
+
redirect = getattr(agent, "redirect", None)
|
|
314
|
+
try:
|
|
315
|
+
redirected = bool(redirect(text)) if callable(redirect) else False
|
|
316
|
+
except Exception as exc: # noqa: BLE001
|
|
317
|
+
log.debug("redirect failed: %s", exc)
|
|
318
|
+
redirected = False
|
|
319
|
+
if not redirected:
|
|
320
|
+
with _steer_slot_lock(agent):
|
|
321
|
+
if _steer_slot(agent) == rest:
|
|
322
|
+
agent._pending_steer = before or None
|
|
323
|
+
return {"redirected": False, "withdrawn": True, "requeued": "kept"}
|
|
324
|
+
agent.steer(text)
|
|
325
|
+
return {"redirected": False, "withdrawn": True, "requeued": "last"}
|
|
326
|
+
with _steer_slot_lock(agent):
|
|
327
|
+
slot = _steer_slot(agent)
|
|
328
|
+
delivery = "steer" if slot == text or slot.endswith("\n" + text) else "redirect"
|
|
329
|
+
return {"redirected": True, "withdrawn": True, "delivery": delivery}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _steer_queue_params(params: dict) -> tuple[list[str], int]:
|
|
333
|
+
pending = params.get("pending")
|
|
334
|
+
index = params.get("index")
|
|
335
|
+
if not isinstance(pending, list) or not all(isinstance(t, str) and t.strip() for t in pending):
|
|
336
|
+
raise InvalidParams("pending must be a list of steer texts")
|
|
337
|
+
if not isinstance(index, int) or isinstance(index, bool) or not 0 <= index < len(pending):
|
|
338
|
+
raise InvalidParams("index must point into pending")
|
|
339
|
+
return [t.strip() for t in pending], index
|
|
267
340
|
|
|
268
341
|
|
|
269
342
|
def _cancel_checkpoint(run: _Run) -> list | None:
|
|
@@ -725,6 +798,131 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
725
798
|
_RUNS_BY_SESSION.pop(session_id, None)
|
|
726
799
|
|
|
727
800
|
|
|
801
|
+
def _checkpoint_required() -> bool:
|
|
802
|
+
"""``compression.checkpoint_required`` needs the memory provider loaded to write the pre-compression checkpoint."""
|
|
803
|
+
try:
|
|
804
|
+
from hermes_cli.config import load_config
|
|
805
|
+
|
|
806
|
+
return ((load_config() or {}).get("compression") or {}).get("checkpoint_required") is True
|
|
807
|
+
except Exception: # noqa: BLE001
|
|
808
|
+
return False
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
#: Compressed results awaiting the server's write: ``commit_token -> (agent, profile home, expiry timer)``.
|
|
812
|
+
_PENDING_COMPRESSIONS: dict[str, tuple[Any, Any, threading.Timer]] = {}
|
|
813
|
+
_PENDING_COMPRESSIONS_LOCK = threading.Lock()
|
|
814
|
+
#: A server that never answers (restart, crash) gets its compression discarded after this long.
|
|
815
|
+
_PENDING_COMPRESSION_TTL = 600.0
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _release_compression(agent, committed: bool) -> None:
|
|
819
|
+
"""Emit (committed) or discard the Agent's deferred context-engine notification, then close the throwaway agent."""
|
|
820
|
+
from agent.conversation_compression import finalize_context_engine_compression_notification
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
finalize_context_engine_compression_notification(agent, committed=committed)
|
|
824
|
+
finally:
|
|
825
|
+
with contextlib.suppress(Exception):
|
|
826
|
+
agent._end_session_on_close = False
|
|
827
|
+
close = getattr(agent, "close", None)
|
|
828
|
+
if callable(close):
|
|
829
|
+
try:
|
|
830
|
+
close()
|
|
831
|
+
except Exception: # noqa: BLE001
|
|
832
|
+
log.debug("compression agent close failed", exc_info=True)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def finalize_compression(token: str, committed: bool) -> bool:
|
|
836
|
+
"""``chat.compress_finalize``: the server reports whether it installed the result; unknown tokens are a no-op."""
|
|
837
|
+
with _PENDING_COMPRESSIONS_LOCK:
|
|
838
|
+
entry = _PENDING_COMPRESSIONS.pop(token, None)
|
|
839
|
+
if entry is None:
|
|
840
|
+
return False
|
|
841
|
+
agent, home, expiry = entry
|
|
842
|
+
expiry.cancel() # a no-op when the timer itself is finalizing
|
|
843
|
+
with scoped_home(home):
|
|
844
|
+
_release_compression(agent, committed=committed)
|
|
845
|
+
return True
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def compress(ctx: CallContext, params: dict) -> dict:
|
|
849
|
+
"""Manual ``/compress`` of ``conversation_history`` through the Agent's shared core (``compress_now``) on a throwaway
|
|
850
|
+
agent, like the gateway's ``_run_manual_compression``. The server owns the transcript: it re-checks the session and
|
|
851
|
+
installs ``messages`` itself, so nothing here touches history or the cached turn agent. The agent has no session
|
|
852
|
+
store, so the Agent neither rotates nor writes state.db; its one deferred effect, the context-engine notification,
|
|
853
|
+
waits for ``chat.compress_finalize`` with the returned ``commit_token`` (two-phase, like the gateway's commit)."""
|
|
854
|
+
home = profile_home_param(params)
|
|
855
|
+
session_id = str(params.get("session_id") or "").strip()
|
|
856
|
+
if not session_id:
|
|
857
|
+
raise InvalidParams("session_id is required")
|
|
858
|
+
history = params.get("conversation_history")
|
|
859
|
+
if not isinstance(history, list):
|
|
860
|
+
raise InvalidParams("conversation_history must be a list")
|
|
861
|
+
model = str(params.get("model") or "").strip()
|
|
862
|
+
provider = str(params.get("model_provider") or "").strip() or None
|
|
863
|
+
focus_topic = str(params.get("focus_topic") or "").strip()[:500] or None
|
|
864
|
+
toolsets = params.get("enabled_toolsets")
|
|
865
|
+
runtime = _resolve_runtime(provider, model)
|
|
866
|
+
if not runtime.get("api_key"):
|
|
867
|
+
raise RpcError("No provider configured -- cannot compress.", condition="credential_missing")
|
|
868
|
+
from agent.conversation_compression_manual import CompressRequest, compress_now
|
|
869
|
+
|
|
870
|
+
AIAgent = _agent_class()
|
|
871
|
+
kwargs: dict = dict(
|
|
872
|
+
model=model or str(runtime.get("model") or ""),
|
|
873
|
+
provider=provider or runtime.get("provider"),
|
|
874
|
+
base_url=runtime.get("base_url"),
|
|
875
|
+
api_key=runtime.get("api_key"),
|
|
876
|
+
platform="webui",
|
|
877
|
+
quiet_mode=True,
|
|
878
|
+
enabled_toolsets=toolsets if isinstance(toolsets, list) else None,
|
|
879
|
+
session_id=session_id,
|
|
880
|
+
)
|
|
881
|
+
for name, value in (
|
|
882
|
+
("api_mode", runtime.get("api_mode")),
|
|
883
|
+
("acp_command", runtime.get("acp_command")),
|
|
884
|
+
("acp_args", runtime.get("acp_args")),
|
|
885
|
+
("credential_pool", runtime.get("credential_pool")),
|
|
886
|
+
("gateway_session_key", session_id),
|
|
887
|
+
("skip_memory", not _checkpoint_required()),
|
|
888
|
+
):
|
|
889
|
+
if _supported(AIAgent, name) and value is not None:
|
|
890
|
+
kwargs[name] = value
|
|
891
|
+
agent = AIAgent(**kwargs)
|
|
892
|
+
held = False
|
|
893
|
+
try:
|
|
894
|
+
result = compress_now(agent, history, CompressRequest(focus_topic=focus_topic), task_id=session_id)
|
|
895
|
+
message = None
|
|
896
|
+
if result.status != "compressed":
|
|
897
|
+
from agent.conversation_compression_manual import render_compress_result
|
|
898
|
+
|
|
899
|
+
message = "\n".join(render_compress_result(result)) or None
|
|
900
|
+
payload = {
|
|
901
|
+
"status": result.status,
|
|
902
|
+
"messages": json.loads(json.dumps([m for m in result.after_messages if isinstance(m, dict)], default=str)),
|
|
903
|
+
"before_tokens": int(result.before_tokens or 0),
|
|
904
|
+
"after_tokens": int(result.after_tokens or 0),
|
|
905
|
+
"summary": json.loads(json.dumps(result.summary, default=str)) if isinstance(result.summary, dict) else None,
|
|
906
|
+
"message": message,
|
|
907
|
+
"agent_session_id": str(getattr(agent, "session_id", None) or session_id),
|
|
908
|
+
"commit_token": None,
|
|
909
|
+
}
|
|
910
|
+
if result.status == "compressed":
|
|
911
|
+
token = uuid.uuid4().hex
|
|
912
|
+
# A server that never answers (restart, failed RPC) gets the compression discarded on its own.
|
|
913
|
+
expiry = threading.Timer(_PENDING_COMPRESSION_TTL, finalize_compression, args=(token, False))
|
|
914
|
+
expiry.daemon = True
|
|
915
|
+
with _PENDING_COMPRESSIONS_LOCK:
|
|
916
|
+
_PENDING_COMPRESSIONS[token] = (agent, home, expiry)
|
|
917
|
+
expiry.start()
|
|
918
|
+
held = True
|
|
919
|
+
payload["commit_token"] = token
|
|
920
|
+
return payload
|
|
921
|
+
finally:
|
|
922
|
+
if not held:
|
|
923
|
+
_release_compression(agent, committed=False)
|
|
924
|
+
|
|
925
|
+
|
|
728
926
|
def _run_for(params: dict) -> _Run | None:
|
|
729
927
|
stream_id = str(params.get("stream_id") or "").strip()
|
|
730
928
|
session_id = str(params.get("session_id") or "").strip()
|
|
@@ -748,6 +946,18 @@ def register(registry) -> None:
|
|
|
748
946
|
with _turn_identity(session_id, workspace):
|
|
749
947
|
return start(ctx, params)
|
|
750
948
|
|
|
949
|
+
@registry.method("chat.compress")
|
|
950
|
+
def compress_(ctx: CallContext, params: dict) -> dict:
|
|
951
|
+
with scoped_home(profile_home_param(params)):
|
|
952
|
+
return compress(ctx, params)
|
|
953
|
+
|
|
954
|
+
@registry.method("chat.compress_finalize", requires_agent=False)
|
|
955
|
+
def compress_finalize_(ctx: CallContext, params: dict) -> dict:
|
|
956
|
+
token = str(params.get("commit_token") or "").strip()
|
|
957
|
+
if not token:
|
|
958
|
+
raise InvalidParams("commit_token is required")
|
|
959
|
+
return {"finalized": finalize_compression(token, params.get("committed") is True)}
|
|
960
|
+
|
|
751
961
|
@registry.method("chat.interrupt", requires_agent=False)
|
|
752
962
|
def interrupt_(ctx: CallContext, params: dict) -> dict:
|
|
753
963
|
run = _run_for(params)
|
|
@@ -786,7 +996,24 @@ def register(registry) -> None:
|
|
|
786
996
|
except Exception as exc: # noqa: BLE001
|
|
787
997
|
log.debug("steer failed: %s", exc)
|
|
788
998
|
return {"accepted": False, "fallback": "steer_error"}
|
|
789
|
-
|
|
999
|
+
# `can_redirect`: whether this Agent can deliver a pending steer now (TAL-424 "Send now").
|
|
1000
|
+
return {"accepted": accepted, "fallback": None if accepted else "not_running", "can_redirect": callable(getattr(run.agent, "redirect", None))}
|
|
1001
|
+
|
|
1002
|
+
@registry.method("chat.steer_withdraw", requires_agent=False)
|
|
1003
|
+
def steer_withdraw_(ctx: CallContext, params: dict) -> dict:
|
|
1004
|
+
pending, index = _steer_queue_params(params)
|
|
1005
|
+
run = _run_for(params)
|
|
1006
|
+
if run is None or run.agent is None:
|
|
1007
|
+
return {"withdrawn": False}
|
|
1008
|
+
return {"withdrawn": withdraw_steer(run.agent, pending, index)}
|
|
1009
|
+
|
|
1010
|
+
@registry.method("chat.steer_now", requires_agent=False)
|
|
1011
|
+
def steer_now_(ctx: CallContext, params: dict) -> dict:
|
|
1012
|
+
pending, index = _steer_queue_params(params)
|
|
1013
|
+
run = _run_for(params)
|
|
1014
|
+
if run is None or run.agent is None:
|
|
1015
|
+
return {"redirected": False, "withdrawn": False}
|
|
1016
|
+
return steer_now(run.agent, pending, index)
|
|
790
1017
|
|
|
791
1018
|
@registry.method("chat.evict_agent", requires_agent=False)
|
|
792
1019
|
def evict_(ctx: CallContext, params: dict) -> dict:
|
|
@@ -379,25 +379,33 @@ def register(registry) -> None:
|
|
|
379
379
|
post["profile"] = str(body["profile"]).strip()
|
|
380
380
|
if body.get("toast_notifications") is False:
|
|
381
381
|
post["toast_notifications"] = False
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if post.get("profile")
|
|
382
|
+
if body.get("owner_profile"):
|
|
383
|
+
post["owner_profile"] = str(body["owner_profile"]).strip()
|
|
384
|
+
execution_home = params.get("execution_home")
|
|
385
|
+
if post.get("profile"):
|
|
386
|
+
if not execution_home:
|
|
387
|
+
raise InvalidParams("execution_home is required for a profile job")
|
|
388
|
+
home = Path(execution_home)
|
|
389
|
+
model, provider = (value.strip() or None if isinstance(value, str) else None for value in (body.get("model"), body.get("provider")))
|
|
390
|
+
# The scheduler ignores Talaria's profile field. Pin the execution profile's main model
|
|
391
|
+
# before creating it in that profile's own store. Scheduled workers bind credentials/config
|
|
392
|
+
# to the store home; owner_profile only preserves management from the creating Web profile.
|
|
393
|
+
if post.get("profile") and execution_home and not (model and provider) and not body.get("no_agent"):
|
|
386
394
|
try:
|
|
387
|
-
from cron.jobs import
|
|
388
|
-
|
|
389
|
-
with _SNAPSHOT_LOCK, scoped_home(Path(
|
|
390
|
-
|
|
391
|
-
if
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
+
from cron.jobs import _main_model_pin
|
|
396
|
+
|
|
397
|
+
with _SNAPSHOT_LOCK, scoped_home(Path(execution_home)):
|
|
398
|
+
profile_provider, profile_model = _main_model_pin()
|
|
399
|
+
if not profile_model:
|
|
400
|
+
raise ValueError("Profile has no main model configured")
|
|
401
|
+
provider = provider or profile_provider
|
|
402
|
+
model = model or profile_model
|
|
395
403
|
except Exception as exc: # noqa: BLE001
|
|
396
|
-
raise RpcError(f"Cannot safely resolve cron
|
|
404
|
+
raise RpcError(f"Cannot safely resolve cron model for profile {post['profile']!r}", condition="cron_snapshot_failed") from exc
|
|
397
405
|
with _store(home), scoped_home(home):
|
|
398
406
|
try:
|
|
399
407
|
job = jobs.create_job(prompt=body.get("prompt") or "", schedule=body["schedule"], name=body.get("name") or None, deliver=body.get("deliver") or "local",
|
|
400
|
-
skills=body.get("skills") or [], model=
|
|
408
|
+
skills=body.get("skills") or [], model=model, provider=provider, **kwargs)
|
|
401
409
|
except Exception as exc: # noqa: BLE001
|
|
402
410
|
raise InvalidParams(str(exc)) from exc
|
|
403
411
|
if post:
|
|
@@ -4,8 +4,13 @@ routing; the sidecar drains and formats registry events."""
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import logging
|
|
8
9
|
import queue
|
|
10
|
+
import sqlite3
|
|
11
|
+
import time
|
|
12
|
+
from contextlib import closing
|
|
13
|
+
from pathlib import Path
|
|
9
14
|
|
|
10
15
|
from ..errors import InvalidParams
|
|
11
16
|
from ..home import profile_home_param, scoped_home
|
|
@@ -89,6 +94,62 @@ def mark_consumed(process_id: str) -> bool:
|
|
|
89
94
|
return False
|
|
90
95
|
|
|
91
96
|
|
|
97
|
+
def _delivery_api():
|
|
98
|
+
"""The Agent's durable delivery ledger (``tools.async_delegation``), or None on an Agent without it."""
|
|
99
|
+
try:
|
|
100
|
+
from tools import async_delegation
|
|
101
|
+
except Exception: # noqa: BLE001
|
|
102
|
+
return None
|
|
103
|
+
names = ("claim_event_delivery", "complete_event_delivery", "release_event_delivery")
|
|
104
|
+
return async_delegation if all(callable(getattr(async_delegation, n, None)) for n in names) else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _ledger_event(evt: dict) -> dict:
|
|
108
|
+
return {k: v for k, v in evt.items() if k not in ("process_id", "consumed")}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def claim_delivery(evt: dict, consumer: str) -> str | None:
|
|
112
|
+
"""Claim a durable delegation completion before delivering it (TAL-459).
|
|
113
|
+
|
|
114
|
+
``None``: another consumer holds it, or the ledger already delivered or dropped it, so do not deliver.
|
|
115
|
+
``""``: nothing durable to acknowledge (interim notice, legacy event, or an Agent without the ledger)."""
|
|
116
|
+
api = _delivery_api()
|
|
117
|
+
if api is None:
|
|
118
|
+
return ""
|
|
119
|
+
claim_id = api.claim_event_delivery(_ledger_event(evt), consumer)
|
|
120
|
+
return None if claim_id is None else str(claim_id)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def complete_delivery(evt: dict, claim_id: str) -> bool:
|
|
124
|
+
api = _delivery_api()
|
|
125
|
+
if api is None or not claim_id:
|
|
126
|
+
return False
|
|
127
|
+
api.complete_event_delivery(_ledger_event(evt), claim_id)
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def release_delivery(evt: dict, claim_id: str) -> bool:
|
|
132
|
+
api = _delivery_api()
|
|
133
|
+
if api is None or not claim_id:
|
|
134
|
+
return False
|
|
135
|
+
api.release_event_delivery(_ledger_event(evt), claim_id)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def defer_delivery(evt: dict, claim_id: str) -> bool:
|
|
140
|
+
"""Hand back a claim the target never admitted (its session was busy) without spending a delivery attempt;
|
|
141
|
+
the Agent drops a row for good after its attempt budget, so a busy session must not burn it."""
|
|
142
|
+
api = _delivery_api()
|
|
143
|
+
if api is None or not claim_id:
|
|
144
|
+
return False
|
|
145
|
+
defer = getattr(api, "defer_completion_delivery", None)
|
|
146
|
+
if not callable(defer) or evt.get("type") != "async_delegation" or not evt.get("delegation_id"):
|
|
147
|
+
api.release_event_delivery(_ledger_event(evt), claim_id)
|
|
148
|
+
return True
|
|
149
|
+
defer(str(evt["delegation_id"]), claim_id)
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
92
153
|
def format_notification(evt: dict) -> str:
|
|
93
154
|
if evt.get("type") == "async_delegation":
|
|
94
155
|
try:
|
|
@@ -122,6 +183,210 @@ def list_sessions() -> list[dict]:
|
|
|
122
183
|
return out
|
|
123
184
|
|
|
124
185
|
|
|
186
|
+
_LEDGER_COLUMNS = "delegation_id, origin_ui_session_id, state, dispatched_at, completed_at, updated_at, task_json, result_json, event_json IS NOT NULL"
|
|
187
|
+
_LEDGER_LIMIT = 200
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _json(text) -> dict:
|
|
191
|
+
try:
|
|
192
|
+
value = json.loads(text) if text else {}
|
|
193
|
+
except (TypeError, ValueError):
|
|
194
|
+
return {}
|
|
195
|
+
return value if isinstance(value, dict) else {}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _unit_goals(task: dict) -> list[str]:
|
|
199
|
+
goals = task.get("goals") if isinstance(task.get("goals"), list) else None
|
|
200
|
+
indexes = task.get("task_indexes") if isinstance(task.get("task_indexes"), list) else None
|
|
201
|
+
if goals and indexes:
|
|
202
|
+
return [str(goals[i]) for i in indexes if isinstance(i, int) and 0 <= i < len(goals)]
|
|
203
|
+
if goals:
|
|
204
|
+
return [str(g) for g in goals]
|
|
205
|
+
return [str(task.get("goal") or "")]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _child_statuses(result: dict) -> list[str]:
|
|
209
|
+
"""Per-subagent outcome of a finished unit: a batch carries ``results``, a single task is its own result."""
|
|
210
|
+
if isinstance(result.get("results"), list):
|
|
211
|
+
return [str(r.get("status") or "") for r in result["results"] if isinstance(r, dict)]
|
|
212
|
+
return [str(result.get("status") or "")] if result else []
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _ledger_rows(home: Path, session_ids: list[str]) -> list[dict]:
|
|
216
|
+
db_path = home / "state.db"
|
|
217
|
+
if not db_path.exists() or not session_ids:
|
|
218
|
+
return []
|
|
219
|
+
marks = ",".join("?" * len(session_ids))
|
|
220
|
+
with closing(sqlite3.connect(f"{db_path.resolve().as_uri()}?mode=ro", uri=True, timeout=5)) as conn:
|
|
221
|
+
try:
|
|
222
|
+
rows = conn.execute(f"SELECT {_LEDGER_COLUMNS} FROM async_delegations WHERE origin_ui_session_id IN ({marks}) ORDER BY dispatched_at DESC LIMIT ?", [*session_ids, _LEDGER_LIMIT]).fetchall()
|
|
223
|
+
except sqlite3.OperationalError: # an Agent that never delegated has no ledger table yet
|
|
224
|
+
return []
|
|
225
|
+
out = []
|
|
226
|
+
for delegation_id, origin, state, dispatched_at, completed_at, updated_at, task_json, result_json, has_event in rows:
|
|
227
|
+
task, result = _json(task_json), _json(result_json)
|
|
228
|
+
goals = _unit_goals(task)
|
|
229
|
+
out.append({
|
|
230
|
+
"delegation_id": delegation_id, "origin_ui_session_id": origin, "state": state, "dispatched_at": dispatched_at,
|
|
231
|
+
"completed_at": completed_at, "updated_at": updated_at, "goals": goals,
|
|
232
|
+
"child_statuses": _child_statuses(result), "has_result": bool(has_event),
|
|
233
|
+
})
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _live_delegations() -> dict[str, dict]:
|
|
238
|
+
try:
|
|
239
|
+
from tools.async_delegation import list_async_delegations
|
|
240
|
+
items = list_async_delegations()
|
|
241
|
+
except Exception: # noqa: BLE001 - an Agent without the live registry reports from the ledger alone
|
|
242
|
+
return {}
|
|
243
|
+
return {str(i.get("delegation_id")): {"status": str(i.get("status") or "")} for i in items if isinstance(i, dict) and i.get("delegation_id")}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _owned_processes(session_ids: set[str]) -> list[dict]:
|
|
247
|
+
"""Notified or watched processes a WebUI session started (``session_key`` is the WebUI session id)."""
|
|
248
|
+
registry = _registry()
|
|
249
|
+
if registry is None:
|
|
250
|
+
return []
|
|
251
|
+
try:
|
|
252
|
+
with registry._lock:
|
|
253
|
+
sessions = [*registry._running.values(), *registry._finished.values()]
|
|
254
|
+
except Exception: # noqa: BLE001
|
|
255
|
+
return []
|
|
256
|
+
out = []
|
|
257
|
+
for proc in sessions:
|
|
258
|
+
if str(getattr(proc, "session_key", "") or "") not in session_ids:
|
|
259
|
+
continue
|
|
260
|
+
watched = bool(getattr(proc, "watch_patterns", None))
|
|
261
|
+
if not (getattr(proc, "notify_on_complete", False) or watched):
|
|
262
|
+
continue
|
|
263
|
+
exited = bool(getattr(proc, "exited", False))
|
|
264
|
+
out.append({
|
|
265
|
+
"process_id": str(proc.id), "session_key": str(proc.session_key), "command": str(getattr(proc, "command", "") or "")[:200],
|
|
266
|
+
"started_at": float(getattr(proc, "started_at", 0) or 0) or None, "exited": exited,
|
|
267
|
+
"exited_at": float(getattr(proc, "exited_at", 0) or 0) or None, "exit_code": getattr(proc, "exit_code", None),
|
|
268
|
+
"completion_reason": str(getattr(proc, "completion_reason", "") or ""), "watched": watched,
|
|
269
|
+
})
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
_CHILD_WINDOW_S = 5
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _live_children() -> list[dict]:
|
|
277
|
+
"""Running subagents from the Agent's in-process registry: the only place a child's session id sits next to its
|
|
278
|
+
delegation id. An Agent without the registry reports none."""
|
|
279
|
+
try:
|
|
280
|
+
from tools.delegate_tool_registry import _active_subagents, _active_subagents_lock
|
|
281
|
+
with _active_subagents_lock:
|
|
282
|
+
records = list(_active_subagents.values())
|
|
283
|
+
except Exception: # noqa: BLE001
|
|
284
|
+
return []
|
|
285
|
+
out = []
|
|
286
|
+
for r in records:
|
|
287
|
+
sid = getattr(r.get("agent"), "session_id", None)
|
|
288
|
+
if isinstance(sid, str) and sid:
|
|
289
|
+
out.append({"delegation_id": str(r.get("delegation_id") or ""), "goal": str(r.get("goal") or ""), "owner": str(r.get("owner_agent_session_id") or ""), "session_id": sid})
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _ledger_children(home: Path, owners: list[str]) -> list[dict]:
|
|
294
|
+
"""Finished subagents' own sessions in state.db: ``source = subagent`` rows tagged ``_delegate_from`` with their
|
|
295
|
+
parent's session, each with its start time and first user message (the child's goal, sent verbatim)."""
|
|
296
|
+
db_path = home / "state.db"
|
|
297
|
+
if not db_path.exists() or not owners:
|
|
298
|
+
return []
|
|
299
|
+
marks = ",".join("?" * len(owners))
|
|
300
|
+
with closing(sqlite3.connect(f"{db_path.resolve().as_uri()}?mode=ro", uri=True, timeout=5)) as conn:
|
|
301
|
+
try:
|
|
302
|
+
# Only these chats' children: a long-lived profile keeps every subagent session it ever ran.
|
|
303
|
+
rows = conn.execute(
|
|
304
|
+
"SELECT s.id, s.model_config, s.started_at, (SELECT m.content FROM messages m WHERE m.session_id = s.id AND m.role = 'user' ORDER BY m.id LIMIT 1)"
|
|
305
|
+
f" FROM sessions s WHERE s.source = 'subagent' AND json_extract(s.model_config, '$._delegate_from') IN ({marks})", owners).fetchall()
|
|
306
|
+
except sqlite3.OperationalError:
|
|
307
|
+
return []
|
|
308
|
+
out = []
|
|
309
|
+
for sid, model_config, started_at, first in rows:
|
|
310
|
+
owner = _json(model_config).get("_delegate_from")
|
|
311
|
+
goal = _goal_text(first)
|
|
312
|
+
if owner in owners and goal:
|
|
313
|
+
out.append({"owner": owner, "session_id": sid, "started_at": started_at, "goal": goal})
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
_CONTENT_JSON_PREFIX = "\x00json:"
|
|
318
|
+
_IMAGE_HINTS = "\n\n[Image attached"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _goal_text(content) -> str:
|
|
322
|
+
"""The goal a child was sent, from its stored first message: a task with images is stored as multimodal parts
|
|
323
|
+
(native image input; the goal is the text part) or as the goal followed by image hints (text input)."""
|
|
324
|
+
if not isinstance(content, str):
|
|
325
|
+
return ""
|
|
326
|
+
if content.startswith(_CONTENT_JSON_PREFIX):
|
|
327
|
+
try:
|
|
328
|
+
parts = json.loads(content[len(_CONTENT_JSON_PREFIX):])
|
|
329
|
+
except ValueError:
|
|
330
|
+
return ""
|
|
331
|
+
texts = [p.get("text") for p in parts if isinstance(p, dict) and p.get("type") == "text"] if isinstance(parts, list) else []
|
|
332
|
+
return str(texts[0]) if texts and texts[0] else ""
|
|
333
|
+
return content.split(_IMAGE_HINTS, 1)[0]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def unit_children(unit: dict, owner: str, live: list[dict], finished: list[dict], now: float) -> list[dict]:
|
|
337
|
+
"""The subagent sessions one delegation unit ran, as ``{goal, session_id}`` per task, in task order. A live registry
|
|
338
|
+
record is exact (same owner, the unit's call id, the task's goal) and is used once. Without one, finished sessions
|
|
339
|
+
of that owner whose goal matches, started in the unit's window, count only when there are exactly as many as tasks
|
|
340
|
+
with that goal; anything else stays unlinked rather than guessed."""
|
|
341
|
+
unit_id, goals = unit["delegation_id"], [g for g in unit["goals"] if g]
|
|
342
|
+
pool = [r for r in live if r["owner"] == owner and r["delegation_id"] and (unit_id == r["delegation_id"] or unit_id.startswith(f"{r['delegation_id']}-"))]
|
|
343
|
+
linked: list[str | None] = []
|
|
344
|
+
used: set[str] = set()
|
|
345
|
+
for goal in goals:
|
|
346
|
+
record = next((r for r in pool if r["goal"] == goal and r["session_id"] not in used), None)
|
|
347
|
+
if record:
|
|
348
|
+
used.add(record["session_id"])
|
|
349
|
+
linked.append(record["session_id"] if record else None)
|
|
350
|
+
start = (unit.get("dispatched_at") or 0) - _CHILD_WINDOW_S
|
|
351
|
+
end = (unit.get("completed_at") or now) + _CHILD_WINDOW_S
|
|
352
|
+
for goal in {g for g, sid in zip(goals, linked) if sid is None}:
|
|
353
|
+
slots = [i for i, (g, sid) in enumerate(zip(goals, linked)) if g == goal and sid is None]
|
|
354
|
+
matches = sorted((c for c in finished if c["owner"] == owner and c["goal"] == goal and c["session_id"] not in used and start <= (c["started_at"] or 0) <= end), key=lambda c: c["started_at"] or 0)
|
|
355
|
+
if len(matches) == len(slots):
|
|
356
|
+
for i, c in zip(slots, matches):
|
|
357
|
+
linked[i] = c["session_id"]
|
|
358
|
+
return [{"goal": g, "session_id": sid} for g, sid in zip(goals, linked) if sid]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def background_list(home: Path, session_ids: list[str]) -> dict:
|
|
362
|
+
"""TAL-372: what the Agent knows about the background work of these WebUI sessions: delegations from the durable
|
|
363
|
+
ledger (with the live registry's status while they run) and notified processes from the process registry.
|
|
364
|
+
TAL-494: each delegation also names the subagent sessions it ran."""
|
|
365
|
+
live = _live_delegations()
|
|
366
|
+
delegations = _ledger_rows(home, session_ids)
|
|
367
|
+
live_children = _live_children()
|
|
368
|
+
finished = _ledger_children(home, session_ids) if delegations else []
|
|
369
|
+
now = time.time()
|
|
370
|
+
for row in delegations:
|
|
371
|
+
row["live_status"] = live.get(row["delegation_id"], {}).get("status")
|
|
372
|
+
row["children"] = unit_children(row, row["origin_ui_session_id"], live_children, finished, now)
|
|
373
|
+
return {"delegations": delegations, "processes": _owned_processes(set(session_ids))}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def delegation_result(home: Path, session_id: str, delegation_id: str) -> str:
|
|
377
|
+
"""The full result of a finished delegation unit, as the Agent words it for its own notification."""
|
|
378
|
+
db_path = home / "state.db"
|
|
379
|
+
if not db_path.exists():
|
|
380
|
+
return ""
|
|
381
|
+
with closing(sqlite3.connect(f"{db_path.resolve().as_uri()}?mode=ro", uri=True, timeout=5)) as conn:
|
|
382
|
+
try:
|
|
383
|
+
row = conn.execute("SELECT event_json FROM async_delegations WHERE delegation_id = ? AND origin_ui_session_id = ?", (delegation_id, session_id)).fetchone()
|
|
384
|
+
except sqlite3.OperationalError:
|
|
385
|
+
return ""
|
|
386
|
+
event = _json(row[0]) if row else {}
|
|
387
|
+
return format_notification(event) if event else ""
|
|
388
|
+
|
|
389
|
+
|
|
125
390
|
def register(registry_) -> None:
|
|
126
391
|
@registry_.method("process.drain")
|
|
127
392
|
def drain_(ctx: CallContext, params: dict) -> dict:
|
|
@@ -142,6 +407,39 @@ def register(registry_) -> None:
|
|
|
142
407
|
raise InvalidParams("process_id is required")
|
|
143
408
|
return {"ok": mark_consumed(process_id)}
|
|
144
409
|
|
|
410
|
+
def _delivery_params(params: dict) -> dict:
|
|
411
|
+
evt = params.get("event")
|
|
412
|
+
if not isinstance(evt, dict):
|
|
413
|
+
raise InvalidParams("event must be an object")
|
|
414
|
+
return evt
|
|
415
|
+
|
|
416
|
+
@registry_.method("process.claim_delivery")
|
|
417
|
+
def claim_(ctx: CallContext, params: dict) -> dict:
|
|
418
|
+
evt = _delivery_params(params)
|
|
419
|
+
consumer = str(params.get("consumer") or "").strip()
|
|
420
|
+
if not consumer:
|
|
421
|
+
raise InvalidParams("consumer is required")
|
|
422
|
+
with scoped_home(profile_home_param(params)):
|
|
423
|
+
return {"claim_id": claim_delivery(evt, consumer)}
|
|
424
|
+
|
|
425
|
+
@registry_.method("process.complete_delivery")
|
|
426
|
+
def complete_(ctx: CallContext, params: dict) -> dict:
|
|
427
|
+
evt = _delivery_params(params)
|
|
428
|
+
with scoped_home(profile_home_param(params)):
|
|
429
|
+
return {"ok": complete_delivery(evt, str(params.get("claim_id") or ""))}
|
|
430
|
+
|
|
431
|
+
@registry_.method("process.release_delivery")
|
|
432
|
+
def release_(ctx: CallContext, params: dict) -> dict:
|
|
433
|
+
evt = _delivery_params(params)
|
|
434
|
+
with scoped_home(profile_home_param(params)):
|
|
435
|
+
return {"ok": release_delivery(evt, str(params.get("claim_id") or ""))}
|
|
436
|
+
|
|
437
|
+
@registry_.method("process.defer_delivery")
|
|
438
|
+
def defer_(ctx: CallContext, params: dict) -> dict:
|
|
439
|
+
evt = _delivery_params(params)
|
|
440
|
+
with scoped_home(profile_home_param(params)):
|
|
441
|
+
return {"ok": defer_delivery(evt, str(params.get("claim_id") or ""))}
|
|
442
|
+
|
|
145
443
|
@registry_.method("process.format_notification")
|
|
146
444
|
def format_(ctx: CallContext, params: dict) -> dict:
|
|
147
445
|
evt = params.get("event")
|
|
@@ -149,6 +447,26 @@ def register(registry_) -> None:
|
|
|
149
447
|
raise InvalidParams("event must be an object")
|
|
150
448
|
return {"text": format_notification(evt)}
|
|
151
449
|
|
|
450
|
+
def _session_ids(params: dict) -> list[str]:
|
|
451
|
+
ids = params.get("session_ids")
|
|
452
|
+
if not isinstance(ids, list) or not all(isinstance(i, str) and i for i in ids):
|
|
453
|
+
raise InvalidParams("session_ids must be a list of session ids")
|
|
454
|
+
return ids
|
|
455
|
+
|
|
456
|
+
@registry_.method("process.background_list")
|
|
457
|
+
def background_list_(ctx: CallContext, params: dict) -> dict:
|
|
458
|
+
ids = _session_ids(params)
|
|
459
|
+
with scoped_home(profile_home_param(params)) as home:
|
|
460
|
+
return background_list(home, ids)
|
|
461
|
+
|
|
462
|
+
@registry_.method("process.delegation_result")
|
|
463
|
+
def delegation_result_(ctx: CallContext, params: dict) -> dict:
|
|
464
|
+
sid, did = str(params.get("session_id") or ""), str(params.get("delegation_id") or "")
|
|
465
|
+
if not sid or not did:
|
|
466
|
+
raise InvalidParams("session_id and delegation_id are required")
|
|
467
|
+
with scoped_home(profile_home_param(params)) as home:
|
|
468
|
+
return {"text": delegation_result(home, sid, did)}
|
|
469
|
+
|
|
152
470
|
@registry_.method("process.list")
|
|
153
471
|
def list_(ctx: CallContext, params: dict) -> dict:
|
|
154
472
|
with scoped_home(profile_home_param(params)):
|