@maudecode/talaria-web 1.15.0 → 1.17.2
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-raw.js +6 -3
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +158 -16
- package/dist/api/automation-router.js +12 -6
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +193 -6
- package/dist/api/chat-router.js +39 -16
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +12 -1
- package/dist/api/git-router.js +60 -38
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +6 -0
- package/dist/api/raw-routes.js +35 -58
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +4 -2
- package/dist/api/router.js +8 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +974 -42
- package/dist/api/sessions-router.js +87 -25
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +254 -11
- package/dist/api/settings-router.js +68 -18
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +11 -3
- package/dist/api/sse-routes.js +145 -65
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.js +6 -2
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +86 -9
- package/dist/api/tools-router.js +19 -15
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +0 -8397
- package/dist/app.js +7 -2
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +11 -4
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +6 -2
- package/dist/auth/oidc.js +16 -11
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/store.d.ts +14 -2
- package/dist/auth/store.js +26 -1
- package/dist/auth/store.js.map +1 -1
- package/dist/cli/dotenv.d.ts +1 -1
- package/dist/cli/dotenv.js +36 -3
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/config/agent-config.d.ts +2 -0
- package/dist/config/agent-config.js +9 -0
- package/dist/config/agent-config.js.map +1 -1
- package/dist/http/addresses.d.ts +6 -0
- package/dist/http/addresses.js +26 -0
- package/dist/http/addresses.js.map +1 -1
- package/dist/http/context.d.ts +6 -3
- package/dist/http/context.js +10 -4
- package/dist/http/context.js.map +1 -1
- package/dist/links.d.ts +26 -0
- package/dist/links.js +60 -0
- package/dist/links.js.map +1 -0
- package/dist/onboarding.js +30 -7
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.js +1 -1
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/providers/catalog.d.ts +167 -5
- package/dist/providers/catalog.js +662 -135
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +9 -4
- package/dist/providers/tables.js.map +1 -1
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +518 -15
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +79 -37
- package/dist/runtime.js.map +1 -1
- package/dist/sessions/anchor.d.ts +24 -3
- package/dist/sessions/anchor.js +94 -44
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +4 -1
- package/dist/sessions/background-tasks.js +7 -4
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/claude-code.d.ts +19 -0
- package/dist/sessions/claude-code.js +250 -0
- package/dist/sessions/claude-code.js.map +1 -0
- package/dist/sessions/completions.d.ts +13 -1
- package/dist/sessions/completions.js +64 -12
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +14 -1
- package/dist/sessions/compress.js +52 -11
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/journal.d.ts +8 -0
- package/dist/sessions/journal.js +44 -10
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +13 -5
- package/dist/sessions/list.js +56 -26
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +122 -9
- package/dist/sessions/merge.js +544 -33
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/pending.d.ts +5 -0
- package/dist/sessions/pending.js +30 -10
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/service.d.ts +100 -4
- package/dist/sessions/service.js +567 -102
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +16 -1
- package/dist/sessions/session.js +29 -2
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.js +11 -2
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/state-db.d.ts +26 -4
- package/dist/sessions/state-db.js +126 -8
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.js +6 -1
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +10 -0
- package/dist/sessions/streams.js +14 -3
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/todo.d.ts +6 -1
- package/dist/sessions/todo.js +11 -4
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +12 -1
- package/dist/sessions/tool-display.js +76 -1
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn.d.ts +49 -10
- package/dist/sessions/turn.js +459 -185
- package/dist/sessions/turn.js.map +1 -1
- package/dist/settings.d.ts +20 -2
- package/dist/settings.js +49 -2
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.d.ts +2 -2
- package/dist/sidecar/fake.js +7 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.js +2 -1
- package/dist/spa.js.map +1 -1
- package/dist/test/harness.js +2 -0
- package/dist/test/harness.js.map +1 -1
- package/dist/tools/commands.d.ts +15 -0
- package/dist/tools/commands.js +52 -0
- package/dist/tools/commands.js.map +1 -0
- package/dist/tools/crons.d.ts +4 -1
- package/dist/tools/crons.js +58 -11
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/insights.d.ts +8 -2
- package/dist/tools/insights.js +18 -2
- package/dist/tools/insights.js.map +1 -1
- package/dist/tools/kanban.d.ts +41 -1
- package/dist/tools/kanban.js +113 -2
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +2 -1
- package/dist/tools/mcp.js +16 -6
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/terminal.d.ts +1 -0
- package/dist/tools/terminal.js +1 -1
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +1 -1
- package/dist/tools/update-notifications.js +1 -1
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/updates.d.ts +15 -3
- package/dist/tools/updates.js +207 -41
- package/dist/tools/updates.js.map +1 -1
- package/dist/workspace/fs.d.ts +7 -3
- package/dist/workspace/fs.js +5 -1
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +57 -10
- package/dist/workspace/git.js +302 -195
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media-refs.d.ts +13 -0
- package/dist/workspace/media-refs.js +165 -0
- package/dist/workspace/media-refs.js.map +1 -0
- package/dist/workspace/media.d.ts +19 -0
- package/dist/workspace/media.js +63 -2
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/preview.d.ts +28 -0
- package/dist/workspace/preview.js +110 -0
- package/dist/workspace/preview.js.map +1 -0
- package/dist/workspace/rollback.d.ts +5 -4
- package/dist/workspace/rollback.js +12 -9
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/upload.d.ts +2 -0
- package/dist/workspace/upload.js +4 -1
- package/dist/workspace/upload.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +7 -1
- package/dist/workspace/workspaces.js +30 -21
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -4
- package/dist/workspace/worktrees.js +28 -33
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +4 -2
- package/sidecar/scripts/replay_sidecar.py +4 -2
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/__main__.py +7 -1
- package/sidecar/talaria_sidecar/home.py +98 -2
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +19 -3
- package/sidecar/talaria_sidecar/methods/chat.py +551 -61
- package/sidecar/talaria_sidecar/methods/commands.py +61 -33
- package/sidecar/talaria_sidecar/methods/config.py +14 -10
- package/sidecar/talaria_sidecar/methods/cron.py +10 -6
- package/sidecar/talaria_sidecar/methods/kanban.py +115 -15
- package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
- package/sidecar/talaria_sidecar/methods/process.py +60 -1
- package/sidecar/talaria_sidecar/methods/profiles.py +12 -5
- package/sidecar/talaria_sidecar/methods/providers.py +161 -1
- package/sidecar/talaria_sidecar/methods/runtime.py +5 -0
- package/sidecar/talaria_sidecar/methods/text.py +18 -21
- package/sidecar/talaria_sidecar/methods/usage.py +264 -5
- package/static/dist/FILES.txt +126 -121
- package/static/dist/assets/AppShell-fb2td9zi.js +1 -0
- package/static/dist/assets/ChatPage-cj96kiy9.js +25 -0
- package/static/dist/assets/Dialog-kxcg7ard.js +1 -0
- package/static/dist/assets/ExtensionRoute-jf7h0754.js +1 -0
- package/static/dist/assets/HubRoute-gmanpd5y.js +2 -0
- package/static/dist/assets/{Markdown-dy8t4dy7.js → Markdown-mdwlvj3w.js} +125 -125
- package/static/dist/assets/RightPanel-ftvrjlcp.js +1 -0
- package/static/dist/assets/Select-bilaypej.js +1 -0
- package/static/dist/assets/SessionListPanel-og8n36qs.js +1 -0
- package/static/dist/assets/SettingsLayout-jonjefmj.js +1 -0
- package/static/dist/assets/States-nmejrwrm.js +1 -0
- package/static/dist/assets/Toaster-nnl8zkt9.js +1 -0
- package/static/dist/assets/_app-gdidzd94.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-opyh9y0v.js +1 -0
- package/static/dist/assets/_app.index-hbarzbjx.js +1 -0
- package/static/dist/assets/_app.insights-luywlarz.js +1 -0
- package/static/dist/assets/_app.kanban-kincnbr8.js +1 -0
- package/static/dist/assets/_app.logs-oi2gr3gs.js +1 -0
- package/static/dist/assets/_app.memory-b6jfivt6.js +1 -0
- package/static/dist/assets/_app.profiles-lpvixmqp.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-gyoh5ylx.js → _app.session._sessionId-bfd4h3id.js} +1 -1
- package/static/dist/assets/_app.settings-vowx3iq0.js +1 -0
- package/static/dist/assets/_app.settings._section-cnntg1tu.js +3 -0
- package/static/dist/assets/_app.skills-fb57lpau.js +1 -0
- package/static/dist/assets/_app.tasks-eszpx1q7.js +2 -0
- package/static/dist/assets/_app.todos-nr05ac6w.js +1 -0
- package/static/dist/assets/_app.workspaces-d2luomu6.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-cjqletra.js → abnfDiagram-VCTEODGH-b2x1lqot.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +1 -0
- package/static/dist/assets/architectureDiagram-5GKGNRK7-h38o5l7b.js +36 -0
- package/static/dist/assets/{back-e4m6nsnm.js → back-bemaer09.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-b195z021.js → blockDiagram-I7D4REHJ-bqquxv2u.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-olu212k1.js → c4Diagram-7LVT6UL2-f3fb8w6z.js} +1 -1
- package/static/dist/assets/cancel-drd0g6d9.js +1 -0
- package/static/dist/assets/channel-cpdgf867.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-b8o1nufs.js → chunk-4HAMMTFA-bzfyyzp3.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-l1seddto.js → chunk-75Z2AOVW-ly99rfx5.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lo5zg15v.js → chunk-DU6HZSFF-bg6ecfqf.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-dsdzrp16.js → chunk-F27PBJKO-e8k7mmqw.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-f3fxqq8b.js → chunk-GMAD6QVW-cmalnoaj.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-h8egxtf6.js → chunk-GVQU2GXP-gmif5o01.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-iolwd8s0.js → chunk-IMKFNOWR-f1nwwh6u.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-b88bbtwi.js → chunk-L3NEJ4N5-oktfh5v7.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-e3e7nr6s.js → chunk-OSK3NFVY-b1sdx96z.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-mdc46rt1.js → chunk-P2QGCYS3-fhu4nzxt.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-kqlllrrm.js → chunk-POPQ4Y6H-em31rnef.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-he585k2x.js → chunk-PWAF6VOD-bxhhyftw.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gh94y9a4.js → chunk-SHT3W25Y-fmrvwcav.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-euxp9y7e.js → chunk-SVP7TREG-lymukl2j.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-c8cxes1q.js → chunk-TICWLB2K-lmza6k23.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +1 -0
- package/static/dist/assets/{copied-kq0u3ht7.js → copied-ima3pysb.js} +1 -1
- package/static/dist/assets/copy-fw5b1pc6.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lxd44ux4.js → cynefin-OW5HDTMX-czlhbyhs.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-njpiwat8.js → cynefinDiagram-5FMLGOSQ-gq7mvswb.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-nvgy6yju.js → dagre-GXQ25YYZ-cqdbf7dr.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-g98nyv72.js → diagram-S7CK7UJ4-o6arxa58.js} +3 -3
- package/static/dist/assets/{diagram-UQ7AKVKN-ovgm6lvv.js → diagram-UQ7AKVKN-nhnd68tz.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-pb2bn2e4.js → diagram-VSXAHHWV-dk2sclzn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-6j4mvfqo.js → diagram-VX7I27RA-n3k5nyx3.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-dqmb982r.js → diagram-Z3DM3KII-m6q9cwdh.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-ozbpzb6d.js → ebnfDiagram-PWID7BFC-lsa5641p.js} +1 -1
- package/static/dist/assets/endpoints-d33siokx.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-hl7m8vua.js → erDiagram-RLTQ6QDP-fo2ckbe6.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ompixqmh.js → ganttDiagram-EL5Y4UJY-nzqz6x9g.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-b6vxwp9q.js → gitGraph-4MIJSDKK-gukvcxe6.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-k98f57on.js → gitGraphDiagram-WWUBYQGX-lqhdp8f0.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-cd3ff6ge.js → highlighted-body-KPVGNVTW-igw6cp39.js} +1 -1
- package/static/dist/assets/index-cxq58xb1.js +11 -0
- package/static/dist/assets/index-hgtx08n5.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-hjcwjami.js → info-A6RAGUB7-gvu7a93l.js} +1 -1
- package/static/dist/assets/infoDiagram-27XIBGKW-m8t8g6w1.js +2 -0
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-bxak4mwu.js → ishikawaDiagram-5VMMS53U-l07vkbgj.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-khmihclr.js → journeyDiagram-3NMN7TZE-hybkgj8l.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h1r32wdx.js → kanban-definition-UXKFOSKX-by13uvtn.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-b2eg0fws.js → lazyRouteComponent-af0lvgjx.js} +1 -1
- package/static/dist/assets/{line-friyjof3.js → line-osxq6fq7.js} +1 -1
- package/static/dist/assets/link-jyxicdhg.js +1 -0
- package/static/dist/assets/login-ec21t39x.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-f1gvpwrt.js → mermaid-parser.core-i91oxmou.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-mt2oxwue.js → mindmap-definition-YA3MSWOX-m82mx8ww.js} +1 -1
- package/static/dist/assets/onboarding-btnltvyz.js +1 -0
- package/static/dist/assets/onboarding_model_label-dp7xffoh.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-jik4ixlf.js → packet-AYTQ26CC-nbtgge2o.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-erxlv6cc.js → pegDiagram-XKGWAZYB-f1zxzga1.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-mnkoldfl.js → pie-WAS4IAKB-of8vuhxs.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-i7lwrada.js → pieDiagram-E7YTZNPT-o7y8ftio.js} +3 -3
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-fr8ejdoy.js → quadrantDiagram-AXDQQJYC-iqiwia1t.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-otarjte2.js → radar-RG4KPBEZ-delnsy2v.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-lw362oto.js → railroad-74A4TZTK-nspnyq9u.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-esyn75pw.js → railroadDiagram-O6MQD6OU-z41tqx5z.js} +1 -1
- package/static/dist/assets/refresh-b4uams1i.js +1 -0
- package/static/dist/assets/refresh-cw-nt1dtsx4.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-cyhi5u29.js → requirementDiagram-BXWQKSXE-bdvif194.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jljpbo0h.js → sankeyDiagram-P5KCCOFB-h73e0rol.js} +1 -1
- package/static/dist/assets/{saved-do49si3v.js → saved-kg52csy7.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fz0dhlu7.js → sequenceDiagram-WJ2MYXX4-iv95mb1z.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-b7lojn0o.js → settings_section_extensions_title-ej0iapxe.js} +1 -1
- package/static/dist/assets/share._token-f97e94r0.js +2 -0
- package/static/dist/assets/sliders-horizontal-fj18it47.js +1 -0
- package/static/dist/assets/sse-fwpqtzbg.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-kdnithdx.js → stateDiagram-D77RDMKH-k417ixmf.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-esd6gmz3.js → swimlanes-42K2YHIH-esola70w.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-g0qygccv.js → timeline-definition-24CTP7MA-getrz2y8.js} +1 -1
- package/static/dist/assets/todoStore-eginf9h4.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-ndchzkuu.js → treeView-Q6P3EWNA-hc7ap1q7.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-hwjxj0zz.js → treemap-WGGIJYW6-b7k1syy5.js} +1 -1
- package/static/dist/assets/tts-lxdxnmjd.js +1 -0
- package/static/dist/assets/{untitled-mbr6tuhu.js → untitled-ovpfthg4.js} +1 -1
- package/static/dist/assets/useForm-bcblv1rx.js +1 -0
- package/static/dist/assets/useLocale-fi5jb8ma.js +1 -0
- package/static/dist/assets/{useParams-lo9u33d1.js → useParams-lhg566ew.js} +1 -1
- package/static/dist/assets/{useSearch-lq1mr090.js → useSearch-lbrh6ejn.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-gubovd1r.js → vennDiagram-4TSXK5OY-n6pcy5qb.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-ir3om65j.js → wardley-WFR3VGLG-lpa18vz1.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ce0or1ie.js → wardleyDiagram-VM6X3IG4-kpclwwg7.js} +2 -2
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-dexylcjg.js → xychartDiagram-S5SC5T6Z-paa2n8y0.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +2 -2
- package/static/dist/assets/AppShell-orat1s43.js +0 -1
- package/static/dist/assets/ChatPage-ojhka3hy.js +0 -22
- package/static/dist/assets/HubRoute-gxgw1t33.js +0 -2
- package/static/dist/assets/RightPanel-c66ani1s.js +0 -1
- package/static/dist/assets/Select-fswf66up.js +0 -1
- package/static/dist/assets/SessionListPanel-goh5mtt8.js +0 -1
- package/static/dist/assets/SettingsLayout-n5d4wq4m.js +0 -1
- package/static/dist/assets/States-kxyu50at.js +0 -1
- package/static/dist/assets/Toaster-6zmbcqgg.js +0 -1
- package/static/dist/assets/_app-fowyruiu.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +0 -1
- package/static/dist/assets/_app.index-c9zv4y7w.js +0 -1
- package/static/dist/assets/_app.insights-jf12nofo.js +0 -1
- package/static/dist/assets/_app.kanban-lri5a4yu.js +0 -1
- package/static/dist/assets/_app.logs-md1p0zqo.js +0 -1
- package/static/dist/assets/_app.memory-bzvf2e28.js +0 -1
- package/static/dist/assets/_app.profiles-jpg7zmbn.js +0 -1
- package/static/dist/assets/_app.settings-mpuwlx4j.js +0 -1
- package/static/dist/assets/_app.settings._section-knz8rwoj.js +0 -1
- package/static/dist/assets/_app.skills-fwu1dkir.js +0 -1
- package/static/dist/assets/_app.tasks-h9gphtl3.js +0 -2
- package/static/dist/assets/_app.todos-hd1dnvrh.js +0 -1
- package/static/dist/assets/_app.workspaces-l8toyn5f.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +0 -1
- package/static/dist/assets/architectureDiagram-5GKGNRK7-k5a7v36n.js +0 -36
- package/static/dist/assets/channel-gozy3dp3.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +0 -1
- package/static/dist/assets/endpoints-k8agnro8.js +0 -9
- package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +0 -1
- package/static/dist/assets/index-nfy0ezs6.css +0 -2
- package/static/dist/assets/index-nqed3oum.js +0 -11
- package/static/dist/assets/infoDiagram-27XIBGKW-fm06rj36.js +0 -2
- package/static/dist/assets/link-m90ahx3n.js +0 -1
- package/static/dist/assets/login-bw7m2fzo.js +0 -1
- package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +0 -1
- package/static/dist/assets/onboarding-ret37j1e.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +0 -1
- package/static/dist/assets/refresh-cw-k66keho6.js +0 -1
- package/static/dist/assets/search-j5ki6a9o.js +0 -1
- package/static/dist/assets/share._token-kg8w647b.js +0 -2
- package/static/dist/assets/sliders-horizontal-c4wya4dy.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +0 -8
- package/static/dist/assets/tabBuild-bjmyvmn5.js +0 -56
- package/static/dist/assets/todoStore-kjk5l0sy.js +0 -1
- package/static/dist/assets/useForm-ocxu460f.js +0 -1
- package/static/dist/assets/useLocale-fzy5jhg6.js +0 -1
package/dist/sessions/turn.js
CHANGED
|
@@ -12,29 +12,57 @@ import { isWithin, resolvePathLikePython } from '../workspace/paths.js';
|
|
|
12
12
|
import { SidecarError } from '../sidecar/client.js';
|
|
13
13
|
import { HttpFailure, markSessionTitleGenerated } from './service.js';
|
|
14
14
|
import { StreamRegistry, SessionChannels } from './streams.js';
|
|
15
|
+
import { ChatUsageSchema } from '@maudecode/talaria-web-contracts';
|
|
15
16
|
import { PendingPrompts, clarifyReply } from './pending.js';
|
|
16
17
|
import { RunJournal } from './journal.js';
|
|
17
|
-
import { CONTEXT_USAGE_FIELDS, Session, titleFrom } from './session.js';
|
|
18
|
-
import {
|
|
18
|
+
import { CONTEXT_USAGE_FIELDS, Session, contextUsage, titleFrom } from './session.js';
|
|
19
|
+
import { anchorMessageKey, compressionReference, markerSummary, visibleMessagesForAnchor } from './compress.js';
|
|
20
|
+
import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactSessionData, redactString, withToolId } from '../redact.js';
|
|
19
21
|
import { dict } from '../config/agent-config.js';
|
|
20
22
|
import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
|
|
21
23
|
import { messageWindowForDisplay, messagesForLimitedPayload, toolCallsForMessageWindow } from './window.js';
|
|
22
|
-
import { attachTodoState } from './todo.js';
|
|
24
|
+
import { UNSETTLED_TODO_KEY, attachTodoState, parseTodoToolResult } from './todo.js';
|
|
23
25
|
import { withSessionWireFlags } from './list.js';
|
|
24
26
|
import { hydrateAnchorActivityScenes, turnTerminalState, withTurnIds } from './anchor.js';
|
|
25
27
|
import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
|
|
26
28
|
import { maxIterationsFromConfig, maxTokensFromConfig, processWakeupMaxIterations, reasoningConfigFromConfig, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
|
|
27
|
-
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, buildPartialMessage,
|
|
29
|
+
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, stoppedTurnContext, toolOutcome, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withToolCallOutcomes, workspaceContextPrefix, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
|
|
28
30
|
import { mayBecomeSilentReply, turnOrigin, withBackgroundUpdates } from './background-updates.js';
|
|
29
31
|
import { fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
|
|
32
|
+
import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
|
|
30
33
|
import { str } from '../util.js';
|
|
34
|
+
import { toolEditDiff } from './tool-display.js';
|
|
31
35
|
export const CHAT_LOCK_WAIT_SECONDS = 2;
|
|
32
36
|
const IMAGE_MODE_TIMEOUT_MS = 15_000;
|
|
33
37
|
const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
|
|
38
|
+
/** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
|
|
39
|
+
const METERING_INTERVAL_S = 1;
|
|
34
40
|
/** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
|
|
35
41
|
const BACKGROUND_UNWIND_WAIT_MS = 30_000;
|
|
42
|
+
const COMPRESSION_EXHAUSTED = { label: 'Context compression exhausted', type: 'compression_exhausted', hint: 'The conversation context is too large to compress safely. Start a new conversation or retry with a narrower task.' };
|
|
43
|
+
/** The Agent's session counters onto a session's: a zero or missing reading keeps the figure it already has. */
|
|
44
|
+
function applyAgentUsage(target, usage) {
|
|
45
|
+
if (usage.prompt_tokens > 0)
|
|
46
|
+
target.input_tokens = usage.prompt_tokens;
|
|
47
|
+
if (usage.completion_tokens > 0)
|
|
48
|
+
target.output_tokens = usage.completion_tokens;
|
|
49
|
+
if (usage.estimated_cost_usd !== null)
|
|
50
|
+
target.estimated_cost = usage.estimated_cost_usd;
|
|
51
|
+
if (usage.cache_read_tokens > 0)
|
|
52
|
+
target.cache_read_tokens = usage.cache_read_tokens;
|
|
53
|
+
if (usage.cache_write_tokens > 0)
|
|
54
|
+
target.cache_write_tokens = usage.cache_write_tokens;
|
|
55
|
+
}
|
|
56
|
+
/** TAL-397: the `metering` frame's counters, the same figures the session persists. */
|
|
57
|
+
const meteringUsage = (c) => ({
|
|
58
|
+
input_tokens: c.input_tokens || 0, output_tokens: c.output_tokens || 0, estimated_cost: typeof c.estimated_cost === 'number' ? c.estimated_cost : null,
|
|
59
|
+
cache_read_tokens: c.cache_read_tokens || 0, cache_write_tokens: c.cache_write_tokens || 0,
|
|
60
|
+
});
|
|
61
|
+
/** `compressionExhausted`: the Agent flagged the turn itself, so its wording does not matter. */
|
|
36
62
|
export function classifyProviderError(errStr, opts = {}) {
|
|
37
63
|
const lower = errStr.toLowerCase();
|
|
64
|
+
if (opts.compressionExhausted)
|
|
65
|
+
return COMPRESSION_EXHAUSTED;
|
|
38
66
|
if (opts.condition === 'credential_missing')
|
|
39
67
|
return { label: 'Authentication failed', type: 'auth_mismatch', hint: 'The selected model may not be supported by your configured provider or your API key is invalid. Run `hermes model` in your terminal to update credentials, then restart the WebUI.' };
|
|
40
68
|
const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
|
|
@@ -60,7 +88,7 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
60
88
|
if (notFound)
|
|
61
89
|
return { label: 'Model not found', type: 'model_not_found', hint: 'The selected model was not found by the provider. Check the model ID in Settings or run `hermes model` to verify it exists for your provider.' };
|
|
62
90
|
if (compressionExhausted)
|
|
63
|
-
return
|
|
91
|
+
return COMPRESSION_EXHAUSTED;
|
|
64
92
|
if (opts.silentFailure)
|
|
65
93
|
return { label: 'No response from provider', type: 'no_response', hint: 'The provider returned no content and no error. This often means a usage/rate limit was hit silently. Check provider status, switch providers via `hermes model`, or try again in a moment.' };
|
|
66
94
|
return { label: 'Error', type: 'error', hint: '' };
|
|
@@ -132,7 +160,27 @@ export function isGenericContinuationIntent(text) {
|
|
|
132
160
|
const parts = normalized.split(' ');
|
|
133
161
|
return parts.length > 0 && parts.length <= 2 && parts.every((p) => GENERIC_CONTINUATION_INTENTS.has(p));
|
|
134
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Python `_aux_title_generation_enabled`: `auxiliary.title_generation.enabled` with the Agent's
|
|
165
|
+
* `is_truthy_value(default=True)` semantics. Missing/null enables; a string enables only as 1/true/yes/on.
|
|
166
|
+
*/
|
|
167
|
+
export function titleGenerationEnabled(cfg) {
|
|
168
|
+
const value = dict(dict(cfg.auxiliary).title_generation).enabled;
|
|
169
|
+
if (value === undefined || value === null)
|
|
170
|
+
return true;
|
|
171
|
+
if (typeof value === 'string')
|
|
172
|
+
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
173
|
+
// Python `bool()`: empty containers are false; any other number than zero (NaN included) is true.
|
|
174
|
+
if (Array.isArray(value))
|
|
175
|
+
return value.length > 0;
|
|
176
|
+
if (typeof value === 'object')
|
|
177
|
+
return Object.keys(value).length > 0;
|
|
178
|
+
if (typeof value === 'number')
|
|
179
|
+
return value !== 0;
|
|
180
|
+
return Boolean(value);
|
|
181
|
+
}
|
|
135
182
|
/** Python `_explicit_text_signal`: `agent.image_input_mode: text` or a configured `auxiliary.vision` backend. */
|
|
183
|
+
const isImageAttachment = (att) => Boolean(str(att.path).trim()) && str(att.mime).trim().startsWith('image/');
|
|
136
184
|
export function explicitTextSignal(cfg) {
|
|
137
185
|
const agent = dict(cfg.agent);
|
|
138
186
|
if (str(agent.image_input_mode ?? 'auto').trim().toLowerCase() === 'text')
|
|
@@ -157,6 +205,10 @@ export class TurnRunner {
|
|
|
157
205
|
steerRewrites = new Map();
|
|
158
206
|
deferredPendingSteer = new Map();
|
|
159
207
|
lastCompletedTool = new Map();
|
|
208
|
+
/** TAL-397: each running turn's live counters, once the Agent reported any; an error or cancel persists them too. */
|
|
209
|
+
liveUsage = new Map();
|
|
210
|
+
/** TAL-397: each running turn's latest todo list, which an error or cancel keeps on the session. */
|
|
211
|
+
liveTodos = new Map();
|
|
160
212
|
/** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
|
|
161
213
|
settledStreams = new Set();
|
|
162
214
|
/**
|
|
@@ -180,6 +232,8 @@ export class TurnRunner {
|
|
|
180
232
|
return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
|
|
181
233
|
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
182
234
|
return { error: `Profile '${str(s.profile)}' is being deleted.`, _status: 409 };
|
|
235
|
+
if (this.deps.workspaceBusy?.(opts.workspace))
|
|
236
|
+
return { error: WORKSPACE_BUSY_MESSAGE, _status: 409 };
|
|
183
237
|
const locked = s.active_stream_id;
|
|
184
238
|
if (locked) {
|
|
185
239
|
if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
|
|
@@ -302,13 +356,16 @@ export class TurnRunner {
|
|
|
302
356
|
}
|
|
303
357
|
const msgText = opts.msg;
|
|
304
358
|
const previousMessages = structuredClone(s.messages);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
359
|
+
// TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
|
|
360
|
+
const startRead = deps.service().stateDbRead(s);
|
|
361
|
+
const previousContext = structuredClone(deps.service().modelContext(s, startRead.rows));
|
|
362
|
+
// Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
|
|
363
|
+
// the Agent gets back the exact bytes it sent (TAL-541).
|
|
364
|
+
const apiHistory = sanitizeMessagesForApi(previousContext, { preserveApiContent: true });
|
|
308
365
|
const workspaceCtx = workspaceContextPrefix(opts.workspace);
|
|
309
366
|
// Before the first await: a Stop can land at any point after admission.
|
|
310
367
|
// An eager save already put this turn's prompt in the transcript; the Stop fallback appends it once itself.
|
|
311
|
-
this.stopContexts.set(streamId, { previousContext: previousContext.filter((m) => m._turn_id !== streamId), historyLength: apiHistory.length, prompt: workspaceCtx + msgText, msgText, checkpointed: false });
|
|
368
|
+
this.stopContexts.set(streamId, { previousContext: previousContext.filter((m) => m._turn_id !== streamId), historyLength: apiHistory.length, prompt: workspaceCtx + msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null });
|
|
312
369
|
const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
|
|
313
370
|
const sidecar = deps.sidecar();
|
|
314
371
|
const partialText = this.registry.partialText.get(streamId) ?? [];
|
|
@@ -322,6 +379,25 @@ export class TurnRunner {
|
|
|
322
379
|
const toolStartedAt = new WeakMap();
|
|
323
380
|
let tokenSent = false;
|
|
324
381
|
let firstTokenAt = null;
|
|
382
|
+
// TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
|
|
383
|
+
const liveUsage = { input_tokens: s.input_tokens, output_tokens: s.output_tokens, estimated_cost: s.estimated_cost, cache_read_tokens: s.cache_read_tokens, cache_write_tokens: s.cache_write_tokens };
|
|
384
|
+
const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
|
|
385
|
+
const putMetering = (force) => {
|
|
386
|
+
const now = deps.now();
|
|
387
|
+
if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
|
|
388
|
+
return;
|
|
389
|
+
meter.sentAt = now;
|
|
390
|
+
const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
|
|
391
|
+
put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
|
|
392
|
+
};
|
|
393
|
+
const meterDelta = () => {
|
|
394
|
+
const now = deps.now();
|
|
395
|
+
if (!meter.deltas)
|
|
396
|
+
meter.first = now;
|
|
397
|
+
meter.last = now;
|
|
398
|
+
meter.deltas += 1;
|
|
399
|
+
putMetering(false);
|
|
400
|
+
};
|
|
325
401
|
// TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
|
|
326
402
|
// ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
|
|
327
403
|
// check the last text segment instead if that shows up.
|
|
@@ -331,7 +407,12 @@ export class TurnRunner {
|
|
|
331
407
|
let capturedTerminalError = null;
|
|
332
408
|
const controller = new AbortController();
|
|
333
409
|
this.abortControllers.set(streamId, controller);
|
|
334
|
-
const
|
|
410
|
+
const attachments = opts.attachments ?? [];
|
|
411
|
+
// TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
|
|
412
|
+
// without native images; only a turn with an image on either side asks for it.
|
|
413
|
+
const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
|
|
414
|
+
const userMessage = this.buildUserMessage(workspaceCtx, msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
415
|
+
const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
|
|
335
416
|
// The prompt the Agent actually gets, native image parts included.
|
|
336
417
|
const stop = this.stopContexts.get(streamId);
|
|
337
418
|
if (stop)
|
|
@@ -375,7 +456,7 @@ export class TurnRunner {
|
|
|
375
456
|
put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
|
|
376
457
|
const result = await sidecar.call('chat.start', {
|
|
377
458
|
profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, workspace: opts.workspace, model: opts.model ?? '', model_provider: opts.modelProvider,
|
|
378
|
-
user_message: userMessage, ...turnContext, conversation_history:
|
|
459
|
+
user_message: userMessage, ...turnContext, conversation_history: conversationHistory, enabled_toolsets: deps.toolsetsFor(s), ...(msgText ? { persist_user_message: msgText } : {}), ...(opts.ephemeral ? { ephemeral: true } : {}),
|
|
379
460
|
}, {
|
|
380
461
|
signal: controller.signal,
|
|
381
462
|
timeoutMs: 0,
|
|
@@ -392,10 +473,12 @@ export class TurnRunner {
|
|
|
392
473
|
partialText.push(str(data.text));
|
|
393
474
|
if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
|
|
394
475
|
heldText += str(data.text);
|
|
476
|
+
meterDelta();
|
|
395
477
|
return;
|
|
396
478
|
}
|
|
397
479
|
put('token', { text: heldText + str(data.text) });
|
|
398
480
|
heldText = '';
|
|
481
|
+
meterDelta();
|
|
399
482
|
return;
|
|
400
483
|
}
|
|
401
484
|
case 'reasoning': {
|
|
@@ -404,6 +487,16 @@ export class TurnRunner {
|
|
|
404
487
|
reasoningText.push(str(data.text));
|
|
405
488
|
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
406
489
|
put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
490
|
+
meterDelta();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
case 'usage': {
|
|
494
|
+
const usage = ChatUsageSchema.safeParse(data);
|
|
495
|
+
if (usage.success) {
|
|
496
|
+
applyAgentUsage(liveUsage, usage.data);
|
|
497
|
+
this.liveUsage.set(streamId, liveUsage);
|
|
498
|
+
}
|
|
499
|
+
putMetering(true);
|
|
407
500
|
return;
|
|
408
501
|
}
|
|
409
502
|
case 'steer_pending':
|
|
@@ -424,20 +517,36 @@ export class TurnRunner {
|
|
|
424
517
|
return;
|
|
425
518
|
}
|
|
426
519
|
case 'tool_complete': {
|
|
427
|
-
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server
|
|
428
|
-
|
|
429
|
-
complete
|
|
520
|
+
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
521
|
+
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
522
|
+
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
|
|
523
|
+
const outcome = toolOutcome(rawResult);
|
|
524
|
+
complete.is_error = outcome.is_error;
|
|
525
|
+
if (rawResult !== undefined)
|
|
526
|
+
complete.result_view = outcome.result_view;
|
|
527
|
+
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
|
|
528
|
+
const editDiff = toolEditDiff(data.name, { diff: resultDiff });
|
|
529
|
+
if (editDiff)
|
|
530
|
+
complete.edit_diff = editDiff;
|
|
430
531
|
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
431
532
|
if (tc) {
|
|
432
533
|
const startedAt = toolStartedAt.get(tc);
|
|
433
534
|
if (startedAt !== undefined)
|
|
434
535
|
complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
|
|
435
|
-
|
|
536
|
+
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
537
|
+
Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
|
|
436
538
|
}
|
|
437
539
|
const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
|
|
438
540
|
this.lastCompletedTool.set(streamId, id);
|
|
439
541
|
const redacted = deps.redactEnabled();
|
|
440
542
|
put('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
|
|
543
|
+
// TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
|
|
544
|
+
const todos = data.name === 'todo' ? parseTodoToolResult(todoResult) : null;
|
|
545
|
+
if (todos) {
|
|
546
|
+
todos.ts = deps.now();
|
|
547
|
+
this.liveTodos.set(streamId, todos);
|
|
548
|
+
put('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
|
|
549
|
+
}
|
|
441
550
|
return;
|
|
442
551
|
}
|
|
443
552
|
// Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
|
|
@@ -447,6 +556,14 @@ export class TurnRunner {
|
|
|
447
556
|
put('approval', { ...(pending ?? data), pending_count });
|
|
448
557
|
return;
|
|
449
558
|
}
|
|
559
|
+
// TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
|
|
560
|
+
case 'approval_resolved': {
|
|
561
|
+
if (!str(data.approval_id))
|
|
562
|
+
return;
|
|
563
|
+
deps.pending.resolveApproval(sessionId, str(data.approval_id));
|
|
564
|
+
put(...this.approvalHeadFrame(sessionId));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
450
567
|
case 'clarify': {
|
|
451
568
|
deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId, kind: 'clarify' });
|
|
452
569
|
const head = deps.pending.clarifyHeadFrame(sessionId);
|
|
@@ -470,33 +587,21 @@ export class TurnRunner {
|
|
|
470
587
|
},
|
|
471
588
|
});
|
|
472
589
|
settledAt.value = true;
|
|
590
|
+
// TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
|
|
591
|
+
const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
|
|
592
|
+
result.messages = withImages(result.messages);
|
|
593
|
+
if (result.context_messages)
|
|
594
|
+
result.context_messages = withImages(result.context_messages);
|
|
473
595
|
if (this.registry.cancelled.has(streamId) || result.status === 'cancelled') {
|
|
474
596
|
// The Stop's pre-interrupt snapshot is the boundary, even when its reply lands after this result (the sidecar
|
|
475
597
|
// answers each request on its own thread); without one, the Agent's interrupted result is its canonical transcript.
|
|
476
598
|
const interrupted = await this.interrupts.get(streamId);
|
|
477
|
-
|
|
599
|
+
applyAgentUsage(liveUsage, result.usage);
|
|
600
|
+
this.liveUsage.set(streamId, liveUsage);
|
|
601
|
+
this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
478
602
|
put('cancel', this.cancelFrame(sessionId));
|
|
479
603
|
return;
|
|
480
604
|
}
|
|
481
|
-
if (opts.ephemeral) {
|
|
482
|
-
let answer = '';
|
|
483
|
-
for (let i = result.messages.length - 1; i >= 0; i -= 1) {
|
|
484
|
-
const m = result.messages[i];
|
|
485
|
-
if (m.role === 'assistant') {
|
|
486
|
-
answer = str(m.content);
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
opts.onDone?.(answer);
|
|
491
|
-
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
492
|
-
put('done', { session: { session_id: sessionId, messages: (result.messages).map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
493
|
-
try {
|
|
494
|
-
rmSync(deps.store.pathFor(sessionId), { force: true });
|
|
495
|
-
}
|
|
496
|
-
catch { /* ignore */ }
|
|
497
|
-
deps.store.sessions.delete(sessionId);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
605
|
// Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
|
|
501
606
|
let current;
|
|
502
607
|
try {
|
|
@@ -512,18 +617,23 @@ export class TurnRunner {
|
|
|
512
617
|
s = current;
|
|
513
618
|
// Python `_maybe_inject_max_iteration_summary_fallback`: an exhausted tool budget leaves the closing
|
|
514
619
|
// explanation in `final_response` only, so it becomes the turn's assistant answer before anything else reads it.
|
|
515
|
-
const
|
|
620
|
+
const withFallback = (rows) => (result.tool_limit_reached ? injectMaxIterationSummaryFallback(rows, result.final_response) : rows);
|
|
621
|
+
const withoutRequest = (rows) => (result.tool_limit_reached ? withoutMaxIterationSummaryRequest(rows, result.max_iterations_summary_request, msgText) : rows);
|
|
622
|
+
const agentRows = withFallback(result.messages);
|
|
623
|
+
const resultMessages = withoutRequest(agentRows);
|
|
516
624
|
// Python `_assistant_reply_added_after_current_turn`: replayed history never counts as this turn's answer (the
|
|
517
625
|
// sidecar reports `completed` whenever a failed run still carries messages).
|
|
518
626
|
// Python's second chance: a turn that emitted no new row still counts when the merged transcript it produced
|
|
519
627
|
// ends on a final answer (the current user row or trailing tool activity makes it "lacking").
|
|
520
628
|
const mergedForCheck = () => mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
|
|
521
|
-
|
|
629
|
+
// TAL-512: a btw answer must be a reply this turn added: everything before it is the parent's replayed history.
|
|
630
|
+
const assistantAdded = assistantReplyAddedAfterCurrentTurn(resultMessages, previousContext, msgText) || (!opts.ephemeral && !sessionLacksFinalAssistantAnswer(mergedForCheck()));
|
|
522
631
|
const lastErr = result.error ?? capturedTerminalError ?? '';
|
|
523
|
-
// Python `
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
632
|
+
// Python `_agent_result_terminal_failure`: the Agent's failed, partial, or compression-exhausted result ends the turn
|
|
633
|
+
// even after text streamed; only a partial with no error text and a final answer still completes.
|
|
634
|
+
const agentFailed = result.failed || result.compression_exhausted || (result.partial && (Boolean(lastErr) || !assistantAdded));
|
|
635
|
+
if (result.status === 'error' || agentFailed || (!assistantAdded && (!tokenSent || opts.ephemeral))) {
|
|
636
|
+
const classification = classifyProviderError(lastErr, { silentFailure: !lastErr, compressionExhausted: result.compression_exhausted });
|
|
527
637
|
const errStr = lastErr || `${classification.label}.`;
|
|
528
638
|
const payload = providerErrorPayload(errStr, classification.type, classification.hint, deps.redactEnabled());
|
|
529
639
|
// Settle the steers first so the persisted turn carries every consumed one; the Agent's pending text is sent next.
|
|
@@ -531,8 +641,10 @@ export class TurnRunner {
|
|
|
531
641
|
await this.steerRewrites.get(streamId);
|
|
532
642
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
|
|
533
643
|
followUp = leftovers;
|
|
534
|
-
this.persistError(s, streamId, classification.label, payload, activeTurnToken);
|
|
535
|
-
|
|
644
|
+
this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages);
|
|
645
|
+
// TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
|
|
646
|
+
if (!opts.ephemeral)
|
|
647
|
+
payload.session = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
|
|
536
648
|
payload.session_id = s.session_id;
|
|
537
649
|
payload.old_session_id = sessionId;
|
|
538
650
|
for (const [event, data] of steerEvents)
|
|
@@ -541,21 +653,57 @@ export class TurnRunner {
|
|
|
541
653
|
failed = true;
|
|
542
654
|
return;
|
|
543
655
|
}
|
|
656
|
+
// TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
|
|
657
|
+
if (opts.ephemeral) {
|
|
658
|
+
let answer = '';
|
|
659
|
+
for (let i = resultMessages.length - 1; i >= 0; i -= 1) {
|
|
660
|
+
const m = resultMessages[i];
|
|
661
|
+
if (m.role === 'assistant') {
|
|
662
|
+
answer = str(m.content);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
opts.onDone?.(answer);
|
|
667
|
+
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
668
|
+
put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
669
|
+
try {
|
|
670
|
+
rmSync(deps.store.pathFor(sessionId), { force: true });
|
|
671
|
+
}
|
|
672
|
+
catch { /* ignore */ }
|
|
673
|
+
deps.store.sessions.delete(sessionId);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
544
676
|
// ── settle the transcript ──
|
|
677
|
+
// TAL-539: after a mid-turn compression the model context is the sidecar's pruned copy, settled like the transcript.
|
|
678
|
+
const pruned = result.compressed ? result.context_messages ?? null : null;
|
|
679
|
+
const context = withoutToolImages(pruned ? withoutRequest(withFallback(pruned)) : resultMessages);
|
|
680
|
+
let estimate = result.compressed ? result.post_compression_context_tokens_estimate ?? null : null;
|
|
681
|
+
// The sidecar estimated its own rows; a tool-limit settlement or stripped screenshots (TAL-544) that changed them move
|
|
682
|
+
// the estimate by the difference.
|
|
683
|
+
if (pruned && estimate !== null && context !== pruned) {
|
|
684
|
+
try {
|
|
685
|
+
const [before, after] = await Promise.all([pruned, context].map((messages) => sidecar.call('models.estimate_tokens', { messages })));
|
|
686
|
+
estimate = Math.max(1, estimate + after.tokens - before.tokens);
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
estimate = null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
545
692
|
// The Agent's last pending-steer text settles the remaining steers before the turn is written back.
|
|
546
693
|
await this.steerRewrites.get(streamId);
|
|
547
694
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
|
|
548
|
-
s.messages = mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
|
|
549
|
-
s.context_messages = dedupeContext(
|
|
550
|
-
for (const m of s.messages) {
|
|
551
|
-
if (m.role !== 'assistant')
|
|
552
|
-
continue;
|
|
553
|
-
if (typeof m.content === 'string')
|
|
554
|
-
m.content = stripXmlToolCalls(m.content);
|
|
555
|
-
}
|
|
695
|
+
s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
|
|
696
|
+
s.context_messages = dedupeContext(context);
|
|
556
697
|
if (result.compressed) {
|
|
557
|
-
s.
|
|
558
|
-
|
|
698
|
+
s.post_compression_context_tokens_estimate = estimate;
|
|
699
|
+
// TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
|
|
700
|
+
// holds no compaction marker, so the summary is the model context's newest one.
|
|
701
|
+
const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
|
|
702
|
+
const after = visibleMessagesForAnchor(s.messages);
|
|
703
|
+
const anchorIdx = before.length ? before.length - 1 : after.length ? 0 : null;
|
|
704
|
+
s.compression_anchor_visible_idx = anchorIdx;
|
|
705
|
+
s.compression_anchor_message_key = anchorMessageKey(anchorIdx !== null && anchorIdx < after.length ? after[anchorIdx] : after.at(-1));
|
|
706
|
+
s.compression_anchor_summary = markerSummary(s.context_messages);
|
|
559
707
|
}
|
|
560
708
|
const now = deps.now();
|
|
561
709
|
for (const m of s.messages)
|
|
@@ -565,22 +713,21 @@ export class TurnRunner {
|
|
|
565
713
|
const usage = result.usage;
|
|
566
714
|
const prevInputTokens = s.input_tokens || 0;
|
|
567
715
|
const prevCacheReadTokens = s.cache_read_tokens || 0;
|
|
568
|
-
|
|
569
|
-
s.input_tokens = usage.prompt_tokens;
|
|
570
|
-
if (usage.completion_tokens > 0)
|
|
571
|
-
s.output_tokens = usage.completion_tokens;
|
|
572
|
-
if (usage.estimated_cost_usd !== null)
|
|
573
|
-
s.estimated_cost = usage.estimated_cost_usd;
|
|
574
|
-
if (usage.cache_read_tokens > 0)
|
|
575
|
-
s.cache_read_tokens = usage.cache_read_tokens;
|
|
576
|
-
if (usage.cache_write_tokens > 0)
|
|
577
|
-
s.cache_write_tokens = usage.cache_write_tokens;
|
|
716
|
+
applyAgentUsage(s, usage);
|
|
578
717
|
if (typeof result.context.context_length === 'number')
|
|
579
718
|
s.context_length = result.context.context_length;
|
|
580
719
|
if (typeof result.context.threshold_tokens === 'number')
|
|
581
720
|
s.threshold_tokens = result.context.threshold_tokens;
|
|
582
721
|
if (typeof result.context.last_prompt_tokens === 'number')
|
|
583
722
|
s.last_prompt_tokens = result.context.last_prompt_tokens;
|
|
723
|
+
if (result.compressed) {
|
|
724
|
+
// TAL-540, Python `_live_usage_snapshot`: the counters and the ring's post-compression figures, so clients update before `done`.
|
|
725
|
+
const usage = {
|
|
726
|
+
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
727
|
+
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
728
|
+
};
|
|
729
|
+
put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
|
|
730
|
+
}
|
|
584
731
|
s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
|
|
585
732
|
s.active_stream_id = null;
|
|
586
733
|
s.pending_user_message = null;
|
|
@@ -611,12 +758,12 @@ export class TurnRunner {
|
|
|
611
758
|
asstIdx += 1;
|
|
612
759
|
if (turnIdx < prevAssistants)
|
|
613
760
|
continue;
|
|
614
|
-
|
|
761
|
+
// The settled row keeps its own fields: only its prose and its inline thinking (into `reasoning`) are rewritten.
|
|
615
762
|
if (typeof m.content === 'string' && m.content) {
|
|
616
|
-
const [content,
|
|
763
|
+
const [content, inline] = splitDisplayText(m.content);
|
|
617
764
|
m.content = content;
|
|
618
|
-
if (
|
|
619
|
-
m.reasoning =
|
|
765
|
+
if (inline)
|
|
766
|
+
m.reasoning = joinReasoning([reasoningFieldsText({ reasoning: m.reasoning }), inline]);
|
|
620
767
|
}
|
|
621
768
|
if (turnIdx === prevAssistants && reasoning.trim() && !m.reasoning)
|
|
622
769
|
m.reasoning = reasoning.trim();
|
|
@@ -637,6 +784,9 @@ export class TurnRunner {
|
|
|
637
784
|
}
|
|
638
785
|
}
|
|
639
786
|
this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
|
|
787
|
+
// TAL-493: the rows this turn's Agent wrote to state.db are now in the transcript, so the merge must not replay them
|
|
788
|
+
// (all of them, the dropped summary request too).
|
|
789
|
+
deps.service().settleStateDb(s, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? previousContext, agentRows, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
|
|
640
790
|
deps.store.save(s);
|
|
641
791
|
deps.pending.clearApprovals(sessionId);
|
|
642
792
|
deps.pending.clearClarifies(sessionId);
|
|
@@ -681,6 +831,8 @@ export class TurnRunner {
|
|
|
681
831
|
};
|
|
682
832
|
if (result.tool_limit_reached)
|
|
683
833
|
donePayload.terminal_reason = 'max_iterations';
|
|
834
|
+
// TAL-397: the live meter ends on the persisted counters and turn rate.
|
|
835
|
+
put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
684
836
|
put('done', donePayload);
|
|
685
837
|
for (const [event, payload] of steerEvents)
|
|
686
838
|
put(event, payload);
|
|
@@ -690,22 +842,16 @@ export class TurnRunner {
|
|
|
690
842
|
this.registry.activeRuns.delete(streamId);
|
|
691
843
|
this.settledStreams.add(streamId);
|
|
692
844
|
this.startSteerFollowUp(sessionId, leftovers);
|
|
693
|
-
|
|
845
|
+
// Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
|
|
846
|
+
await deps.syncUsage?.(s, usedModel || null);
|
|
847
|
+
// Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
|
|
848
|
+
const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
|
|
849
|
+
put('stream_end', { session_id: sessionId });
|
|
850
|
+
for (const r of work)
|
|
851
|
+
if (r.status === 'rejected')
|
|
852
|
+
throw r.reason;
|
|
694
853
|
// Python: the last non-error assistant reply with content, else "(no answer produced)".
|
|
695
|
-
|
|
696
|
-
let answer = '';
|
|
697
|
-
for (let i = s.messages.length - 1; i >= 0; i -= 1) {
|
|
698
|
-
const m = s.messages[i];
|
|
699
|
-
if (m.role !== 'assistant' || m._error)
|
|
700
|
-
continue;
|
|
701
|
-
const text = messageText(m.content).trim();
|
|
702
|
-
if (text) {
|
|
703
|
-
answer = text;
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
opts.onDone(answer || '(no answer produced)');
|
|
708
|
-
}
|
|
854
|
+
opts.onDone?.(lastAnswer(s.messages) || '(no answer produced)');
|
|
709
855
|
}
|
|
710
856
|
catch (error) {
|
|
711
857
|
if (settledAt.value && !(error instanceof SidecarError)) {
|
|
@@ -737,7 +883,8 @@ export class TurnRunner {
|
|
|
737
883
|
for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
|
|
738
884
|
put(event, data);
|
|
739
885
|
this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
740
|
-
|
|
886
|
+
if (!opts.ephemeral)
|
|
887
|
+
payload.session = redactSessionData(this.terminalSessionPayload(current), deps.redactEnabled());
|
|
741
888
|
}
|
|
742
889
|
}
|
|
743
890
|
catch (persistError) {
|
|
@@ -757,45 +904,49 @@ export class TurnRunner {
|
|
|
757
904
|
}
|
|
758
905
|
}
|
|
759
906
|
/**
|
|
760
|
-
* Python `
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
* and its transcript row a distinct identity.
|
|
907
|
+
* Python `_resolve_image_input_mode`: the Agent's image mode for this turn's model under its profile config, or null
|
|
908
|
+
* when it is unknown (no sidecar, a cancel, a failed lookup). A canonical "text" is honoured only with an explicit
|
|
909
|
+
* text signal or a model KNOWN to be text-only; an unknown/custom model forwards natively and lets the Agent's retry
|
|
910
|
+
* guard downgrade.
|
|
765
911
|
*/
|
|
766
|
-
async
|
|
767
|
-
const text = workspaceCtx + msgText;
|
|
768
|
-
const named = attachments.map((att) => str(att.path).trim()).filter(Boolean).map(escapeWorkspacePrefixPath);
|
|
769
|
-
const withFiles = () => (named.length ? `${text}\n\n[Attached files: ${named.join(', ')}]` : text);
|
|
770
|
-
const candidates = attachments.filter((att) => str(att.path).trim() && str(att.mime).trim().startsWith('image/'));
|
|
771
|
-
if (!candidates.length)
|
|
772
|
-
return withFiles();
|
|
912
|
+
async imageInputMode(sessionId, s, opts, signal) {
|
|
773
913
|
const sidecar = this.deps.sidecar();
|
|
774
|
-
if (!sidecar)
|
|
775
|
-
return withFiles();
|
|
776
914
|
// A cancel that landed before this point is final: never start the lookup or wait on it.
|
|
777
|
-
if (signal.aborted)
|
|
778
|
-
return
|
|
915
|
+
if (!sidecar || signal.aborted)
|
|
916
|
+
return null;
|
|
917
|
+
// The session's provider before the Agent canonicalizes it, so a `custom:<name>` override entry still matches.
|
|
918
|
+
const provider = str(opts.modelProvider ?? s.model_provider);
|
|
779
919
|
try {
|
|
780
920
|
// A cancelled turn must not sit behind this lookup: the abort wins the race and `run()` then takes the cancelled path.
|
|
781
|
-
const lookup = sidecar.call('text.image_mode', { profile_home: this.deps.profileHome(s.profile), provider
|
|
921
|
+
const lookup = sidecar.call('text.image_mode', { profile_home: this.deps.profileHome(s.profile), provider, model: str(opts.model ?? s.model), requested_provider: provider }, { signal, timeoutMs: IMAGE_MODE_TIMEOUT_MS });
|
|
782
922
|
const mode = await Promise.race([lookup, new Promise((_, reject) => { if (signal.aborted) {
|
|
783
923
|
reject(new Error('turn cancelled'));
|
|
784
924
|
return;
|
|
785
925
|
} signal.addEventListener('abort', () => { reject(new Error('turn cancelled')); }, { once: true }); })]);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (explicitTextSignal(cfg) || mode.supports_vision === false)
|
|
791
|
-
return withFiles();
|
|
792
|
-
}
|
|
926
|
+
if (mode.mode === 'native')
|
|
927
|
+
return 'native';
|
|
928
|
+
const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
929
|
+
return explicitTextSignal(cfg) || mode.supports_vision === false ? 'text' : 'native';
|
|
793
930
|
}
|
|
794
931
|
catch (error) {
|
|
795
932
|
if (!signal.aborted)
|
|
796
933
|
this.deps.log(`[webui] image mode lookup failed for ${sessionId}: ${error.message}`);
|
|
797
|
-
return
|
|
934
|
+
return null;
|
|
798
935
|
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Python `_build_user_message`: image attachments are embedded as native `image_url` parts only when the turn's
|
|
939
|
+
* image mode is `native` (text mode routes them through the Agent's vision tool path), and only after the bytes are
|
|
940
|
+
* read through an anchored descriptor and sniffed as a real image format. Every attached file is also named by path
|
|
941
|
+
* after the text (TAL-276), so an attachment-only turn still gives the model a request and its transcript row a
|
|
942
|
+
* distinct identity.
|
|
943
|
+
*/
|
|
944
|
+
buildUserMessage(workspaceCtx, msgText, attachments, workspace, sessionId, imageMode) {
|
|
945
|
+
const text = workspaceCtx + msgText;
|
|
946
|
+
const withFiles = () => attachedFilesPrompt(text, attachments);
|
|
947
|
+
const candidates = attachments.filter(isImageAttachment);
|
|
948
|
+
if (!candidates.length || imageMode !== 'native')
|
|
949
|
+
return withFiles();
|
|
799
950
|
const parts = [];
|
|
800
951
|
let images = 0;
|
|
801
952
|
const roots = [workspace, this.deps.attachmentDir(sessionId)].map((r) => resolvePathLikePython(r));
|
|
@@ -835,21 +986,24 @@ export class TurnRunner {
|
|
|
835
986
|
* msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
|
|
836
987
|
*/
|
|
837
988
|
terminalSessionPayload(s) {
|
|
838
|
-
const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor }), this.registry.liveIds);
|
|
989
|
+
const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor, modelOptionFor: this.deps.service().deps.modelOptionFor }), this.registry.liveIds);
|
|
839
990
|
payload.assistant_name = this.deps.service().assistantName(s);
|
|
840
|
-
|
|
991
|
+
payload.workspace_name = this.deps.service().workspaceNames()(s);
|
|
992
|
+
const service = this.deps.service();
|
|
993
|
+
const scened = service.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(withAttachmentObjects(s.messages))), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: true }));
|
|
841
994
|
const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
|
|
842
|
-
const limited = messagesForLimitedPayload(window);
|
|
995
|
+
const limited = messagesForLimitedPayload(withBodyExcerpts(withDisplayMedia(window, service.mediaProjector(s)), s.active_stream_id));
|
|
843
996
|
payload.messages = limited;
|
|
844
997
|
payload.message_count = s.messages.length;
|
|
845
998
|
payload._messages_offset = offset;
|
|
846
999
|
payload._messages_truncated = offset > 0;
|
|
847
|
-
attachTodoState(payload, s.messages);
|
|
1000
|
+
attachTodoState(payload, s.messages, s.extra[UNSETTLED_TODO_KEY]);
|
|
848
1001
|
payload.tool_calls = toolCallsForMessageWindow(s.tool_calls, offset, limited.length);
|
|
1002
|
+
payload.compression_reference = compressionReference(s, scened);
|
|
849
1003
|
return payload;
|
|
850
1004
|
}
|
|
851
1005
|
/** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
|
|
852
|
-
persistError(s, streamId, label, payload, activeTurnToken) {
|
|
1006
|
+
persistError(s, streamId, label, payload, activeTurnToken, agentRows = []) {
|
|
853
1007
|
const startedAt = s.pending_started_at;
|
|
854
1008
|
this.materializePendingUserTurn(s, activeTurnToken, streamId);
|
|
855
1009
|
const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
|
|
@@ -879,13 +1033,18 @@ export class TurnRunner {
|
|
|
879
1033
|
else if (payload.type === 'interrupted')
|
|
880
1034
|
errorMessage.provider_details_label = 'Interruption details';
|
|
881
1035
|
s.messages.push(errorMessage);
|
|
1036
|
+
this.keepLiveState(s, streamId);
|
|
882
1037
|
this.persistConsumedSteers(s, streamId, startedAt, this.deps.now());
|
|
1038
|
+
const stop = this.stopContexts.get(streamId);
|
|
1039
|
+
this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null });
|
|
883
1040
|
try {
|
|
884
1041
|
this.deps.store.save(s);
|
|
885
1042
|
}
|
|
886
1043
|
catch (error) {
|
|
887
1044
|
this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
|
|
888
1045
|
}
|
|
1046
|
+
// The kept live counters are the session's usage now, so the insights row follows them (the runtime logs failures).
|
|
1047
|
+
void this.deps.syncUsage?.(s, str(s.model) || null);
|
|
889
1048
|
this.deps.pending.clearApprovals(s.session_id);
|
|
890
1049
|
this.deps.pending.clearClarifies(s.session_id);
|
|
891
1050
|
this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
|
|
@@ -974,8 +1133,19 @@ export class TurnRunner {
|
|
|
974
1133
|
// TAL-460: a background turn stops quietly: its prompt and model context are kept, but no partial and no Stop row,
|
|
975
1134
|
// and its first settlement is final.
|
|
976
1135
|
const quiet = this.registry.activeRuns.get(streamId)?.origin === 'background';
|
|
977
|
-
if (quiet && current.active_stream_id === null)
|
|
1136
|
+
if (quiet && current.active_stream_id === null) {
|
|
1137
|
+
// TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
|
|
1138
|
+
if (checkpoint) {
|
|
1139
|
+
this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? [], agentRows: checkpoint, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
|
|
1140
|
+
try {
|
|
1141
|
+
this.deps.store.save(current);
|
|
1142
|
+
}
|
|
1143
|
+
catch {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
978
1147
|
return true;
|
|
1148
|
+
}
|
|
979
1149
|
const stop = this.stopContexts.get(streamId);
|
|
980
1150
|
const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
|
|
981
1151
|
const settle = () => {
|
|
@@ -989,9 +1159,13 @@ export class TurnRunner {
|
|
|
989
1159
|
// The worker's canonical result can arrive after cancel() settled without a checkpoint (the interrupt reply failed
|
|
990
1160
|
// or timed out): it replaces this stream's context and never adds a second Stop row. A settlement that already had
|
|
991
1161
|
// one keeps it, so work the Agent finished after the Stop stays out.
|
|
992
|
-
const
|
|
993
|
-
|
|
1162
|
+
const ownStop = current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId);
|
|
1163
|
+
const late = canonical && !stop.checkpointed && ownStop ? settle() : null;
|
|
1164
|
+
if (late)
|
|
994
1165
|
current.context_messages = dedupeContext(late);
|
|
1166
|
+
// TAL-493: the worker's rows committed after the first settlement are covered by what it reports; others are kept.
|
|
1167
|
+
if (late || (ownStop && checkpoint)) {
|
|
1168
|
+
this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null });
|
|
995
1169
|
try {
|
|
996
1170
|
this.deps.store.save(current);
|
|
997
1171
|
}
|
|
@@ -1015,13 +1189,16 @@ export class TurnRunner {
|
|
|
1015
1189
|
this.appendPartialSnapshot(current, streamId);
|
|
1016
1190
|
current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
|
|
1017
1191
|
}
|
|
1192
|
+
this.keepLiveState(current, streamId);
|
|
1018
1193
|
this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
|
|
1194
|
+
this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null });
|
|
1019
1195
|
try {
|
|
1020
1196
|
this.deps.store.save(current);
|
|
1021
1197
|
}
|
|
1022
1198
|
catch {
|
|
1023
1199
|
return false;
|
|
1024
1200
|
}
|
|
1201
|
+
void this.deps.syncUsage?.(current, str(current.model) || null);
|
|
1025
1202
|
this.deps.pending.clearApprovals(current.session_id);
|
|
1026
1203
|
this.deps.pending.clearClarifies(current.session_id);
|
|
1027
1204
|
this.deps.events.publish('session_cancel', { profile: current.profile, sessionId: current.session_id });
|
|
@@ -1031,7 +1208,18 @@ export class TurnRunner {
|
|
|
1031
1208
|
emitToSession(sessionId, event, data) {
|
|
1032
1209
|
this.sessionPuts.get(sessionId)?.(event, data);
|
|
1033
1210
|
}
|
|
1211
|
+
/** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
|
|
1212
|
+
keepLiveState(s, streamId) {
|
|
1213
|
+
const usage = this.liveUsage.get(streamId);
|
|
1214
|
+
if (usage)
|
|
1215
|
+
Object.assign(s, usage);
|
|
1216
|
+
const todos = this.liveTodos.get(streamId);
|
|
1217
|
+
if (todos)
|
|
1218
|
+
s.extra[UNSETTLED_TODO_KEY] = todos;
|
|
1219
|
+
}
|
|
1034
1220
|
teardown(sessionId, streamId) {
|
|
1221
|
+
this.liveUsage.delete(streamId);
|
|
1222
|
+
this.liveTodos.delete(streamId);
|
|
1035
1223
|
const writer = this.writers.get(streamId);
|
|
1036
1224
|
if (writer) {
|
|
1037
1225
|
try {
|
|
@@ -1099,13 +1287,14 @@ export class TurnRunner {
|
|
|
1099
1287
|
}
|
|
1100
1288
|
return { title: '', status, rawPreview: '' };
|
|
1101
1289
|
}
|
|
1102
|
-
/**
|
|
1290
|
+
/**
|
|
1291
|
+
* Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
|
|
1292
|
+
* `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
|
|
1293
|
+
*/
|
|
1103
1294
|
async generateTitle(s, opts = {}) {
|
|
1104
1295
|
const [userText, assistantText] = opts.preferLatest ? latestExchangeSnippets(s.messages) : firstExchangeSnippets(s.messages, { scanPastConsecutiveUsers: true });
|
|
1105
1296
|
if (!userText)
|
|
1106
1297
|
return { title: null, status: 'empty_user_message', rawPreview: '' };
|
|
1107
|
-
if (!this.deps.titleGenerationEnabled())
|
|
1108
|
-
return { title: null, status: 'title_generation_disabled', rawPreview: '' };
|
|
1109
1298
|
let next = '';
|
|
1110
1299
|
let llmStatus = 'empty_assistant_message';
|
|
1111
1300
|
let rawPreview = '';
|
|
@@ -1131,65 +1320,66 @@ export class TurnRunner {
|
|
|
1131
1320
|
const status = (status, reason = '', title = '', rawPreview = '') => {
|
|
1132
1321
|
put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1133
1322
|
};
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1323
|
+
if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText)
|
|
1324
|
+
return;
|
|
1325
|
+
if (s.manual_title) {
|
|
1326
|
+
status('skipped', 'manual_title', placeholder);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
// A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
|
|
1330
|
+
const cfg = await this.deps.profileConfig?.(s.profile ?? null);
|
|
1331
|
+
if (!cfg) {
|
|
1332
|
+
status('skipped', 'config_unavailable', placeholder);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (!titleGenerationEnabled(cfg)) {
|
|
1336
|
+
status('skipped', 'title_generation_disabled', placeholder);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const generated = await this.llmTitle(s, userText, assistantText);
|
|
1340
|
+
const { status: llmStatus, rawPreview } = generated;
|
|
1341
|
+
let next = generated.title;
|
|
1342
|
+
let source = llmStatus;
|
|
1343
|
+
if (!next) {
|
|
1344
|
+
const fallback = fallbackTitleFromExchange(userText, assistantText);
|
|
1345
|
+
if (fallback && !isGenericFallbackTitle(fallback)) {
|
|
1346
|
+
next = fallback;
|
|
1347
|
+
source = 'fallback';
|
|
1140
1348
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1349
|
+
}
|
|
1350
|
+
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1351
|
+
let current = s;
|
|
1352
|
+
try {
|
|
1353
|
+
current = this.deps.store.get(sessionId);
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
current = s;
|
|
1357
|
+
}
|
|
1358
|
+
let effective = str(current.title).trim();
|
|
1359
|
+
let wrote = false;
|
|
1360
|
+
if (next) {
|
|
1361
|
+
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
|
|
1362
|
+
if (current.manual_title || !stillAuto) {
|
|
1363
|
+
status('skipped', 'manual_title', effective);
|
|
1143
1364
|
return;
|
|
1144
1365
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1366
|
+
if (next !== effective) {
|
|
1367
|
+
current.title = next;
|
|
1368
|
+
markSessionTitleGenerated(current);
|
|
1369
|
+
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1370
|
+
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1371
|
+
await this.deps.syncTitle?.(current);
|
|
1372
|
+
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1373
|
+
effective = next;
|
|
1374
|
+
wrote = true;
|
|
1155
1375
|
}
|
|
1156
|
-
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1157
|
-
let current = s;
|
|
1158
|
-
try {
|
|
1159
|
-
current = this.deps.store.get(sessionId);
|
|
1160
|
-
}
|
|
1161
|
-
catch {
|
|
1162
|
-
current = s;
|
|
1163
|
-
}
|
|
1164
|
-
let effective = str(current.title).trim();
|
|
1165
|
-
let wrote = false;
|
|
1166
|
-
if (next) {
|
|
1167
|
-
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
|
|
1168
|
-
if (current.manual_title || !stillAuto) {
|
|
1169
|
-
status('skipped', 'manual_title', effective);
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
if (next !== effective) {
|
|
1173
|
-
current.title = next;
|
|
1174
|
-
markSessionTitleGenerated(current);
|
|
1175
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1176
|
-
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1177
|
-
await this.deps.syncTitle?.(current);
|
|
1178
|
-
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1179
|
-
effective = next;
|
|
1180
|
-
wrote = true;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
if (wrote) {
|
|
1184
|
-
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1185
|
-
put('title', { session_id: sessionId, title: effective });
|
|
1186
|
-
}
|
|
1187
|
-
else
|
|
1188
|
-
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1189
1376
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1377
|
+
if (wrote) {
|
|
1378
|
+
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1379
|
+
put('title', { session_id: sessionId, title: effective });
|
|
1192
1380
|
}
|
|
1381
|
+
else
|
|
1382
|
+
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1193
1383
|
}
|
|
1194
1384
|
// ── cancel / steer ───────────────────────────────────────────────────────
|
|
1195
1385
|
/**
|
|
@@ -1503,6 +1693,60 @@ export class TurnRunner {
|
|
|
1503
1693
|
}
|
|
1504
1694
|
this.deps.log(`[webui] WARNING: steer follow-up for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1505
1695
|
}
|
|
1696
|
+
/**
|
|
1697
|
+
* TAL-396 (Agent gateway `_post_turn_goal_continuation`): after every turn settles, the Agent's GoalManager judges it
|
|
1698
|
+
* while a goal is active and the server starts the continuation turn it asks for. Any turn counts, so a resumed or
|
|
1699
|
+
* persisted goal picks up from the next user message. A user turn that took the session meanwhile runs first and is
|
|
1700
|
+
* judged next. A stopped or failed turn never gets here, so the run ends there.
|
|
1701
|
+
*/
|
|
1702
|
+
async continueGoal(s, streamId, put) {
|
|
1703
|
+
const sidecar = this.deps.sidecar();
|
|
1704
|
+
const lastResponse = lastAnswer(s.messages);
|
|
1705
|
+
// The Agent never drives a goal from an empty reply.
|
|
1706
|
+
if (!sidecar || !lastResponse)
|
|
1707
|
+
return;
|
|
1708
|
+
const sessionId = s.session_id;
|
|
1709
|
+
// Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
|
|
1710
|
+
const goalTurn = this.registry.goalRelated.has(streamId);
|
|
1711
|
+
if (goalTurn)
|
|
1712
|
+
put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' });
|
|
1713
|
+
let decision;
|
|
1714
|
+
try {
|
|
1715
|
+
decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true });
|
|
1716
|
+
}
|
|
1717
|
+
catch (error) {
|
|
1718
|
+
this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
|
|
1719
|
+
if (goalTurn)
|
|
1720
|
+
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (!goalTurn && decision.verdict === 'inactive')
|
|
1724
|
+
return;
|
|
1725
|
+
const prompt = str(decision.continuation_prompt).trim();
|
|
1726
|
+
const proceed = decision.should_continue && Boolean(prompt);
|
|
1727
|
+
const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
|
|
1728
|
+
put('goal', { session_id: sessionId, state: proceed ? 'continuing' : 'idle', ...status });
|
|
1729
|
+
if (!proceed)
|
|
1730
|
+
return;
|
|
1731
|
+
let current;
|
|
1732
|
+
try {
|
|
1733
|
+
current = this.deps.store.get(sessionId);
|
|
1734
|
+
}
|
|
1735
|
+
catch {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const started = this.start(current, { msg: prompt, attachments: [], workspace: current.workspace, model: current.model, modelProvider: current.model_provider, goalRelated: true });
|
|
1739
|
+
if (started._status === 409 && started.active_stream_id) {
|
|
1740
|
+
this.registry.goalRelated.add(started.active_stream_id);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (started._status !== undefined && started._status >= 400) {
|
|
1744
|
+
this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1745
|
+
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
|
|
1749
|
+
}
|
|
1506
1750
|
/** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
|
|
1507
1751
|
settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
1508
1752
|
for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
|
|
@@ -1645,6 +1889,7 @@ export class TurnRunner {
|
|
|
1645
1889
|
}
|
|
1646
1890
|
const profileHome = this.deps.profileHome(s?.profile ?? null);
|
|
1647
1891
|
let resolved = false;
|
|
1892
|
+
let withdrawn = false;
|
|
1648
1893
|
if (entry) {
|
|
1649
1894
|
if (!sidecar)
|
|
1650
1895
|
return relayFailure('not running');
|
|
@@ -1656,8 +1901,13 @@ export class TurnRunner {
|
|
|
1656
1901
|
this.deps.log(`[webui] approval relay failed for ${sessionId}: ${error.message}`);
|
|
1657
1902
|
return relayFailure(str(error.message));
|
|
1658
1903
|
}
|
|
1659
|
-
if (resolved
|
|
1904
|
+
if (resolved)
|
|
1660
1905
|
this.deps.pending.resolveApproval(sessionId, str(entry.approval_id));
|
|
1906
|
+
else {
|
|
1907
|
+
// TAL-514: a rejected answer can mean the Agent already stopped waiting on this card; drop what it no longer holds.
|
|
1908
|
+
await this.reconcileApprovals(sessionId, sidecar);
|
|
1909
|
+
withdrawn = !this.deps.pending.hasApprovalId(sessionId, str(entry.approval_id));
|
|
1910
|
+
}
|
|
1661
1911
|
}
|
|
1662
1912
|
let yoloEnabled;
|
|
1663
1913
|
if (enableYolo) {
|
|
@@ -1680,10 +1930,34 @@ export class TurnRunner {
|
|
|
1680
1930
|
yoloEnabled = true;
|
|
1681
1931
|
this.deps.pending.clearApprovals(sessionId);
|
|
1682
1932
|
}
|
|
1933
|
+
if (withdrawn)
|
|
1934
|
+
return { ok: true, choice, stale_cleared: true, pending_count: this.deps.pending.approvalPending(sessionId).pending_count, ...(enableYolo ? { yolo_enabled: yoloEnabled } : {}) };
|
|
1683
1935
|
if (!found && !this.deps.pending.hasPendingApproval(sessionId))
|
|
1684
1936
|
return { ok: true, choice, stale_cleared: true, ...(enableYolo ? { yolo_enabled: yoloEnabled } : {}) };
|
|
1685
1937
|
return { ok: resolved || !approvalId, choice, ...(enableYolo && (resolved || !approvalId) ? { yolo_enabled: yoloEnabled } : {}) };
|
|
1686
1938
|
}
|
|
1939
|
+
/** The live chat frame after approvals were withdrawn: the new queue head, or `approval_cleared` once none is left. */
|
|
1940
|
+
approvalHeadFrame(sessionId) {
|
|
1941
|
+
const { pending, pending_count } = this.deps.pending.approvalPending(sessionId);
|
|
1942
|
+
return pending ? ['approval', { ...pending, pending_count }] : ['approval_cleared', { session_id: sessionId, pending_count: 0 }];
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Drop mirrored approvals the Agent no longer holds; a failed read keeps them (the card stays answerable). Only
|
|
1946
|
+
* entries queued before the read are candidates: one the Agent parks meanwhile can be missing from its snapshot.
|
|
1947
|
+
*/
|
|
1948
|
+
async reconcileApprovals(sessionId, sidecar) {
|
|
1949
|
+
const before = this.deps.pending.approvalRequestIds(sessionId);
|
|
1950
|
+
let live;
|
|
1951
|
+
try {
|
|
1952
|
+
live = new Set((await sidecar.call('approval.pending', { session_id: sessionId })).pending.map((p) => str(p.request_id)));
|
|
1953
|
+
}
|
|
1954
|
+
catch (error) {
|
|
1955
|
+
this.deps.log(`[webui] approval reconcile failed for ${sessionId}: ${error.message}`);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
|
|
1959
|
+
this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
|
|
1960
|
+
}
|
|
1687
1961
|
withYoloLock(sessionId, fn) {
|
|
1688
1962
|
const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(fn);
|
|
1689
1963
|
const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
|
|
@@ -1736,6 +2010,18 @@ export class TurnRunner {
|
|
|
1736
2010
|
return { ok: true, response };
|
|
1737
2011
|
}
|
|
1738
2012
|
}
|
|
2013
|
+
/** The turn's answer: the last non-error assistant reply with content. */
|
|
2014
|
+
function lastAnswer(messages) {
|
|
2015
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
2016
|
+
const m = messages[i];
|
|
2017
|
+
if (m.role !== 'assistant' || m._error)
|
|
2018
|
+
continue;
|
|
2019
|
+
const text = messageText(m.content).trim();
|
|
2020
|
+
if (text)
|
|
2021
|
+
return text;
|
|
2022
|
+
}
|
|
2023
|
+
return '';
|
|
2024
|
+
}
|
|
1739
2025
|
function previousStartedAt(s, run) {
|
|
1740
2026
|
return typeof s.pending_started_at === 'number' && s.pending_started_at > 0 ? s.pending_started_at : (run?.started_at ?? Date.now() / 1000);
|
|
1741
2027
|
}
|
|
@@ -1743,18 +2029,6 @@ function previousStartedAt(s, run) {
|
|
|
1743
2029
|
function isCancelMarker(m) {
|
|
1744
2030
|
return m._error === true && (m._terminal_state === 'cancelled' || str(m.content).startsWith('**Task cancelled:**'));
|
|
1745
2031
|
}
|
|
1746
|
-
function dedupeContext(messages) {
|
|
1747
|
-
const out = [];
|
|
1748
|
-
let lastKey = null;
|
|
1749
|
-
for (const m of messages) {
|
|
1750
|
-
const key = messageIdentity(m);
|
|
1751
|
-
if (key !== null && key === lastKey && m.role === 'assistant')
|
|
1752
|
-
continue;
|
|
1753
|
-
out.push(m);
|
|
1754
|
-
lastKey = key;
|
|
1755
|
-
}
|
|
1756
|
-
return out;
|
|
1757
|
-
}
|
|
1758
2032
|
export { HttpFailure };
|
|
1759
2033
|
/** Where a consumed steer goes: past the tool results of the call it followed, else past the turn's prompt. */
|
|
1760
2034
|
function roundDuration(seconds) {
|