@maudecode/talaria-web 1.18.0 → 1.19.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 +8 -8
- package/dist/api/auth-raw.js +16 -16
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +2 -2
- package/dist/api/automation-raw.js +32 -32
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +102 -22
- package/dist/api/automation-router.js +15 -13
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1205 -34
- package/dist/api/chat-router.js +158 -78
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +2 -2
- package/dist/api/git-router.js +29 -29
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +3 -3
- package/dist/api/raw-routes.js +63 -61
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +6 -6
- package/dist/api/router.js +22 -22
- package/dist/api/router.js.map +1 -1
- package/dist/api/session-visibility.d.ts +4 -4
- package/dist/api/session-visibility.js +26 -16
- package/dist/api/session-visibility.js.map +1 -1
- package/dist/api/sessions-router.d.ts +1928 -124
- package/dist/api/sessions-router.js +177 -173
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +427 -9
- package/dist/api/settings-router.js +48 -32
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +2 -2
- package/dist/api/sse-routes.js +63 -63
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +58 -33
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +663 -91
- package/dist/api/tools-router.js +309 -52
- package/dist/api/tools-router.js.map +1 -1
- package/dist/api/transcribe-stream.d.ts +16 -0
- package/dist/api/transcribe-stream.js +172 -0
- package/dist/api/transcribe-stream.js.map +1 -0
- package/dist/app.d.ts +4 -1
- package/dist/app.js +66 -47
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +10 -9
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/passkeys.d.ts +15 -14
- package/dist/auth/passkeys.js +89 -91
- package/dist/auth/passkeys.js.map +1 -1
- package/dist/auth/store.d.ts +17 -7
- package/dist/auth/store.js +93 -52
- package/dist/auth/store.js.map +1 -1
- package/dist/bin/talaria-web.js +12 -81
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/ctl.js +2 -1
- package/dist/cli/ctl.js.map +1 -1
- package/dist/cli/dotenv.d.ts +5 -0
- package/dist/cli/dotenv.js +14 -2
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/cli/launcher.js +5 -5
- package/dist/cli/launcher.js.map +1 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +85 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/supervise.d.ts +2 -0
- package/dist/cli/supervise.js +67 -4
- package/dist/cli/supervise.js.map +1 -1
- package/dist/config/agent-config.d.ts +34 -11
- package/dist/config/agent-config.js +113 -58
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/fs/atomic.d.ts +1 -0
- package/dist/fs/atomic.js +4 -1
- package/dist/fs/atomic.js.map +1 -1
- package/dist/fs/stat.d.ts +1 -0
- package/dist/fs/stat.js +11 -0
- package/dist/fs/stat.js.map +1 -0
- package/dist/http/context.d.ts +14 -9
- package/dist/http/context.js +29 -20
- package/dist/http/context.js.map +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +10 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +10 -4
- package/dist/profiles/profiles.js +77 -30
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/projects.d.ts +15 -6
- package/dist/projects.js +55 -47
- package/dist/projects.js.map +1 -1
- package/dist/providers/catalog.d.ts +78 -12
- package/dist/providers/catalog.js +445 -141
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/env-file.d.ts +10 -3
- package/dist/providers/env-file.js +24 -75
- package/dist/providers/env-file.js.map +1 -1
- package/dist/providers/runtime-env.d.ts +4 -2
- package/dist/providers/runtime-env.js +9 -6
- package/dist/providers/runtime-env.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +4 -2
- package/dist/providers/tables.js.map +1 -1
- package/dist/redact.js +18 -9
- package/dist/redact.js.map +1 -1
- package/dist/release.d.ts +7 -0
- package/dist/release.js +14 -0
- package/dist/release.js.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.js +233 -90
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +9 -3
- package/dist/sessions/anchor.js +28 -36
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-events.d.ts +2 -0
- package/dist/sessions/background-events.js +10 -0
- package/dist/sessions/background-events.js.map +1 -0
- package/dist/sessions/background-tasks.d.ts +26 -20
- package/dist/sessions/background-tasks.js +88 -86
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/background-updates.d.ts +15 -8
- package/dist/sessions/background-updates.js +101 -25
- package/dist/sessions/background-updates.js.map +1 -1
- package/dist/sessions/cli-sessions.d.ts +15 -5
- package/dist/sessions/cli-sessions.js +62 -43
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +30 -2
- package/dist/sessions/completions.js +294 -53
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +8 -0
- package/dist/sessions/compress.js +2 -2
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/drafts.d.ts +14 -10
- package/dist/sessions/drafts.js +38 -43
- package/dist/sessions/drafts.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +46 -0
- package/dist/sessions/gateway-sessions.js +378 -0
- package/dist/sessions/gateway-sessions.js.map +1 -0
- package/dist/sessions/goals.d.ts +31 -0
- package/dist/sessions/goals.js +75 -0
- package/dist/sessions/goals.js.map +1 -0
- package/dist/sessions/journal.d.ts +5 -79
- package/dist/sessions/journal.js +15 -606
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +27 -18
- package/dist/sessions/list.js +133 -131
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +68 -10
- package/dist/sessions/merge.js +259 -51
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/notices.d.ts +22 -0
- package/dist/sessions/notices.js +72 -0
- package/dist/sessions/notices.js.map +1 -0
- package/dist/sessions/pending.d.ts +10 -4
- package/dist/sessions/pending.js +83 -7
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/prefix-index.d.ts +82 -0
- package/dist/sessions/prefix-index.js +232 -0
- package/dist/sessions/prefix-index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +4 -7
- package/dist/sessions/recovery.js +97 -281
- package/dist/sessions/recovery.js.map +1 -1
- package/dist/sessions/relay.d.ts +20 -20
- package/dist/sessions/relay.js +205 -127
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +162 -59
- package/dist/sessions/service.js +1133 -521
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +18 -2
- package/dist/sessions/session.js +29 -5
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.d.ts +7 -6
- package/dist/sessions/shares.js +22 -45
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/source-kind.js +1 -0
- package/dist/sessions/source-kind.js.map +1 -1
- package/dist/sessions/state-db-identity.d.ts +18 -0
- package/dist/sessions/state-db-identity.js +67 -0
- package/dist/sessions/state-db-identity.js.map +1 -0
- package/dist/sessions/state-db-worker.js +3 -1
- package/dist/sessions/state-db-worker.js.map +1 -1
- package/dist/sessions/state-db.d.ts +53 -8
- package/dist/sessions/state-db.js +576 -50
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/storage/backend.d.ts +162 -0
- package/dist/sessions/storage/backend.js +19 -0
- package/dist/sessions/storage/backend.js.map +1 -0
- package/dist/sessions/storage/conformance.d.ts +20 -0
- package/dist/sessions/storage/conformance.js +237 -0
- package/dist/sessions/storage/conformance.js.map +1 -0
- package/dist/sessions/storage/document.d.ts +16 -0
- package/dist/sessions/storage/document.js +44 -0
- package/dist/sessions/storage/document.js.map +1 -0
- package/dist/sessions/storage/json-documents.d.ts +23 -0
- package/dist/sessions/storage/json-documents.js +233 -0
- package/dist/sessions/storage/json-documents.js.map +1 -0
- package/dist/sessions/storage/json-files.d.ts +12 -0
- package/dist/sessions/storage/json-files.js +124 -0
- package/dist/sessions/storage/json-files.js.map +1 -0
- package/dist/sessions/storage/json-installation.d.ts +3 -0
- package/dist/sessions/storage/json-installation.js +31 -0
- package/dist/sessions/storage/json-installation.js.map +1 -0
- package/dist/sessions/storage/json-journal.d.ts +114 -0
- package/dist/sessions/storage/json-journal.js +686 -0
- package/dist/sessions/storage/json-journal.js.map +1 -0
- package/dist/sessions/storage/json-recovery.d.ts +4 -0
- package/dist/sessions/storage/json-recovery.js +195 -0
- package/dist/sessions/storage/json-recovery.js.map +1 -0
- package/dist/sessions/storage/json.d.ts +68 -0
- package/dist/sessions/storage/json.js +258 -0
- package/dist/sessions/storage/json.js.map +1 -0
- package/dist/sessions/storage/row.d.ts +6 -0
- package/dist/sessions/storage/row.js +49 -0
- package/dist/sessions/storage/row.js.map +1 -0
- package/dist/sessions/storage/search-text.d.ts +4 -0
- package/dist/sessions/storage/search-text.js +16 -0
- package/dist/sessions/storage/search-text.js.map +1 -0
- package/dist/sessions/store.d.ts +63 -73
- package/dist/sessions/store.js +305 -611
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +4 -0
- package/dist/sessions/streams.js +9 -1
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.js +1 -1
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +5 -0
- package/dist/sessions/todo.js +28 -21
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +5 -4
- package/dist/sessions/tool-display.js +22 -16
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +10 -1
- package/dist/sessions/turn-context.js +7 -4
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +87 -17
- package/dist/sessions/turn.js +795 -347
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +8 -13
- package/dist/sessions/wakeup-pause.js +31 -49
- package/dist/sessions/wakeup-pause.js.map +1 -1
- package/dist/sessions/window.js +13 -12
- package/dist/sessions/window.js.map +1 -1
- package/dist/settings.d.ts +19 -7
- package/dist/settings.js +179 -183
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/client.d.ts +17 -3
- package/dist/sidecar/client.js +78 -8
- package/dist/sidecar/client.js.map +1 -1
- package/dist/sidecar/discover.d.ts +2 -1
- package/dist/sidecar/discover.js +93 -10
- package/dist/sidecar/discover.js.map +1 -1
- package/dist/sidecar/env.d.ts +4 -0
- package/dist/sidecar/env.js +3 -0
- package/dist/sidecar/env.js.map +1 -0
- package/dist/sidecar/fake.js +19 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/test/env-file.d.ts +5 -0
- package/dist/test/env-file.js +88 -0
- package/dist/test/env-file.js.map +1 -0
- package/dist/test/harness.d.ts +1 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/test/passkeys.d.ts +22 -0
- package/dist/test/passkeys.js +62 -0
- package/dist/test/passkeys.js.map +1 -0
- package/dist/test/storage.d.ts +15 -0
- package/dist/test/storage.js +26 -0
- package/dist/test/storage.js.map +1 -0
- package/dist/tools/commands.d.ts +6 -1
- package/dist/tools/commands.js +8 -2
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/connectors.d.ts +33 -0
- package/dist/tools/connectors.js +121 -0
- package/dist/tools/connectors.js.map +1 -0
- package/dist/tools/crons.d.ts +38 -1
- package/dist/tools/crons.js +82 -15
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +5 -5
- package/dist/tools/dashboard-plugins.js +14 -14
- package/dist/tools/dashboard-plugins.js.map +1 -1
- package/dist/tools/extensions.d.ts +5 -4
- package/dist/tools/extensions.js +213 -207
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/gateway-restart.d.ts +62 -0
- package/dist/tools/gateway-restart.js +142 -0
- package/dist/tools/gateway-restart.js.map +1 -0
- package/dist/tools/health.d.ts +15 -3
- package/dist/tools/health.js +97 -34
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/hygiene.d.ts +4 -3
- package/dist/tools/hygiene.js +23 -9
- package/dist/tools/hygiene.js.map +1 -1
- package/dist/tools/kanban.d.ts +4 -2
- package/dist/tools/kanban.js +28 -19
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/memory-masks.d.ts +5 -0
- package/dist/tools/memory-masks.js +256 -0
- package/dist/tools/memory-masks.js.map +1 -0
- package/dist/tools/memory.d.ts +29 -3
- package/dist/tools/memory.js +69 -27
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/plugin-settings.d.ts +15 -0
- package/dist/tools/plugin-settings.js +90 -0
- package/dist/tools/plugin-settings.js.map +1 -0
- package/dist/tools/skills.d.ts +9 -4
- package/dist/tools/skills.js +44 -13
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +34 -15
- package/dist/tools/update-notifications.js +141 -57
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/update-rollback.d.ts +11 -0
- package/dist/tools/update-rollback.js +28 -0
- package/dist/tools/update-rollback.js.map +1 -0
- package/dist/tools/updates.d.ts +93 -33
- package/dist/tools/updates.js +1051 -345
- package/dist/tools/updates.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/git.d.ts +16 -1
- package/dist/workspace/git.js +291 -11
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +12 -9
- package/dist/workspace/media.js +31 -20
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/rollback.d.ts +2 -2
- package/dist/workspace/rollback.js +5 -5
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +15 -19
- package/dist/workspace/workspaces.js +55 -127
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.js +170 -32
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +5 -3
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +26 -0
- package/sidecar/scripts/record-fixtures.py +12 -3
- package/sidecar/scripts/replay_sidecar.py +8 -0
- package/sidecar/scripts/suite_runner.py +302 -0
- package/sidecar/scripts/test.sh +12 -4
- package/sidecar/talaria_sidecar/__init__.py +6 -1
- package/sidecar/talaria_sidecar/__main__.py +4 -2
- package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
- package/sidecar/talaria_sidecar/agent_services.py +1053 -0
- package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
- package/sidecar/talaria_sidecar/home.py +43 -1
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +139 -31
- package/sidecar/talaria_sidecar/methods/chat.py +847 -134
- package/sidecar/talaria_sidecar/methods/commands.py +213 -4
- package/sidecar/talaria_sidecar/methods/config.py +123 -4
- package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
- package/sidecar/talaria_sidecar/methods/cron.py +366 -34
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +194 -54
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
- package/sidecar/talaria_sidecar/methods/goals.py +81 -41
- package/sidecar/talaria_sidecar/methods/kanban.py +80 -27
- package/sidecar/talaria_sidecar/methods/memory.py +84 -0
- package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
- package/sidecar/talaria_sidecar/methods/process.py +160 -11
- package/sidecar/talaria_sidecar/methods/profiles.py +44 -6
- package/sidecar/talaria_sidecar/methods/providers.py +196 -26
- package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
- package/sidecar/talaria_sidecar/methods/skills.py +67 -4
- package/sidecar/talaria_sidecar/methods/state_db.py +277 -70
- package/sidecar/talaria_sidecar/methods/stt.py +159 -78
- package/sidecar/talaria_sidecar/methods/text.py +14 -1
- package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
- package/sidecar/talaria_sidecar/methods/usage.py +113 -14
- package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
- package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
- package/sidecar/talaria_sidecar/rpc.py +45 -4
- package/sidecar/talaria_sidecar/runtime.py +81 -12
- package/sidecar/talaria_sidecar/speed.py +35 -0
- package/static/dist/FILES.txt +126 -127
- package/static/dist/assets/AppShell-cey33v9s.js +8 -0
- package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
- package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
- package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
- package/static/dist/assets/Field-j8uqoigz.js +9 -0
- package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
- package/static/dist/assets/Markdown-lahuichm.js +598 -0
- package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
- package/static/dist/assets/Select-gqatm2e0.js +1 -0
- package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
- package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
- package/static/dist/assets/States-e7dkgtxf.js +1 -0
- package/static/dist/assets/Toaster-bfqqy786.js +1 -0
- package/static/dist/assets/_app-n1n6pfx1.js +1 -0
- package/static/dist/assets/{_app.ext._extensionId-gfbzpoou.js → _app.ext._extensionId-buxjxav3.js} +1 -1
- package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
- package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
- package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
- package/static/dist/assets/_app.logs-dm873z90.js +1 -0
- package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
- package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-fn1bhde6.js → _app.session._sessionId-g2vzghvc.js} +1 -1
- package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
- package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
- package/static/dist/assets/_app.skills-cii852yq.js +1 -0
- package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
- package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
- package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-c8kdixke.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-cku06x1p.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
- package/static/dist/assets/archive-b1j004vn.js +1 -0
- package/static/dist/assets/back-g74gkozu.js +1 -0
- package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
- package/static/dist/assets/channel-es95o4y8.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-ofajdixh.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-e7gkikcw.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
- package/static/dist/assets/copied-bli9tbcx.js +1 -0
- package/static/dist/assets/{copy-magtofyp.js → copy-jamtd1pf.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-siqlulkl.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-i648tah9.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-mwn6uc17.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-mekq7zgq.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-xe6r5qfw.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-nq07dg7k.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-c62ipm3g.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-h39kmnzy.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
- package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-fr2i7caa.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-jwpop9p8.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
- package/static/dist/assets/host-hzm8n85n.js +1 -0
- package/static/dist/assets/index-drdcaqeq.css +2 -0
- package/static/dist/assets/index-jzm844he.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-h11x0dxo.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-dfh47dw9.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
- package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
- package/static/dist/assets/{line-meizqsvn.js → line-h0ojzsvs.js} +1 -1
- package/static/dist/assets/link-k01ryaf9.js +1 -0
- package/static/dist/assets/login-bsae49ew.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-ntne45i9.js → mermaid-parser.core-bnre27u8.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
- package/static/dist/assets/onboarding-c65tysya.js +1 -0
- package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-l6jdka8p.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/{pegDiagram-XKGWAZYB-g1er3bxj.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-i0ylcrt9.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-fbz1e55z.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-f9b70aa4.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-lgsuoeda.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-eqpy9fnz.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
- package/static/dist/assets/{refresh-cw-hlwb02qv.js → refresh-cw-mqbkv2a3.js} +1 -1
- package/static/dist/assets/refresh-l29ljp76.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ogf6puhi.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
- package/static/dist/assets/saved-ndd4t2xb.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
- package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
- package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
- package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-fyoznpcs.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-oefldhfy.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
- package/static/dist/assets/toast-i3h34v6o.js +64 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-f7hcci5o.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nl85071a.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
- package/static/dist/assets/untitled-oaoya4qj.js +1 -0
- package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
- package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-gjc70qru.js} +1 -1
- package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-e6ae539u.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-h5hzbk0n.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-jftvpm3e.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hedh85ge.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
- package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
- package/static/dist/assets/Dialog-g355qxhp.js +0 -1
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
- package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
- package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
- package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
- package/static/dist/assets/Select-kddq75qc.js +0 -1
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
- package/static/dist/assets/States-eq6fe29m.js +0 -1
- package/static/dist/assets/Toaster-pc247u9f.js +0 -1
- package/static/dist/assets/_app-f8n1i0m1.js +0 -1
- package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
- package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
- package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
- package/static/dist/assets/_app.logs-i69qds98.js +0 -1
- package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
- package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
- package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
- package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
- package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
- package/static/dist/assets/back-h7aun7ew.js +0 -1
- package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
- package/static/dist/assets/channel-b2zo73z1.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
- package/static/dist/assets/copied-hcplv46p.js +0 -1
- package/static/dist/assets/endpoints-klbxod4t.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
- package/static/dist/assets/host-i828xuo7.js +0 -1
- package/static/dist/assets/index-eh8zilnv.css +0 -2
- package/static/dist/assets/index-r648f4o1.js +0 -11
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
- package/static/dist/assets/link-oot2yn73.js +0 -1
- package/static/dist/assets/login-gu28l9ou.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
- package/static/dist/assets/refresh-abtu93ke.js +0 -1
- package/static/dist/assets/saved-girnabkf.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
- package/static/dist/assets/share._token-kl0r7ido.js +0 -2
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
- package/static/dist/assets/sse-iwku0sjo.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
- package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
- package/static/dist/assets/units-kgxzxfhi.js +0 -1
- package/static/dist/assets/untitled-cikfjntn.js +0 -1
- package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
- package/static/dist/assets/useLocale-o59tbiea.js +0 -1
|
@@ -14,21 +14,26 @@ import contextlib
|
|
|
14
14
|
import contextvars
|
|
15
15
|
import copy
|
|
16
16
|
import hashlib
|
|
17
|
+
import importlib.util
|
|
17
18
|
import inspect
|
|
18
19
|
import json
|
|
19
20
|
import logging
|
|
20
21
|
import os
|
|
22
|
+
import re
|
|
21
23
|
import threading
|
|
22
24
|
import time
|
|
23
25
|
import uuid
|
|
24
26
|
from collections import OrderedDict
|
|
27
|
+
from functools import lru_cache
|
|
25
28
|
from pathlib import Path
|
|
26
29
|
from typing import Any
|
|
27
30
|
|
|
28
31
|
from ..errors import InvalidParams, RpcError
|
|
29
|
-
from ..home import profile_home_param, scoped_home
|
|
32
|
+
from ..home import profile_home_param, scoped_home, session_workspace
|
|
30
33
|
from ..rpc import CallContext
|
|
31
34
|
from .checkpoints import checkpoint_kwargs
|
|
35
|
+
from .providers import runtime_failure
|
|
36
|
+
from .toolsets import disabled_toolsets
|
|
32
37
|
|
|
33
38
|
log = logging.getLogger("talaria_sidecar.chat")
|
|
34
39
|
|
|
@@ -37,10 +42,10 @@ _AGENT_CACHE_MAX = 32
|
|
|
37
42
|
_AGENT_RELEASE_TIMEOUT = 4.0
|
|
38
43
|
# Profile deletion waits this long for the profile's memory flushes, under the server's 120 s call timeout.
|
|
39
44
|
_PROFILE_RELEASE_TIMEOUT = 60.0
|
|
45
|
+
_SESSION_FINALIZE_TIMEOUT = 10.0
|
|
40
46
|
_TOOL_RESULT_SNIPPET_MAX = 4000
|
|
41
47
|
# Predecessor ``_TOOL_ARG_CONTENT_KEYS`` (#4928): card content / diff-reconstruction inputs keep the long cap.
|
|
42
48
|
_TOOL_ARG_CONTENT_KEYS = frozenset({"command", "cmd", "script", "code", "patch", "diff", "old_string", "new_string", "content", "path", "file_path"})
|
|
43
|
-
_CLARIFY_FALLBACK = "The user did not provide a response within the time limit. Use your best judgement to make the choice and proceed."
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
class _Run:
|
|
@@ -56,6 +61,10 @@ class _Run:
|
|
|
56
61
|
self.lock = threading.Lock()
|
|
57
62
|
# The Agent's transcript list from before this turn: a Stop while the Agent still holds it has no checkpoint.
|
|
58
63
|
self.prior_messages = None
|
|
64
|
+
# TAL-897: the session ended (clear/delete) or the sidecar is draining, set under both cache and run locks; an
|
|
65
|
+
# agent this run binds afterwards forks no review and stays out of the cache.
|
|
66
|
+
self.session_ended = False
|
|
67
|
+
self.home = None # the profile home its agent runs under, set by ``start``
|
|
59
68
|
|
|
60
69
|
|
|
61
70
|
class _ClarifyEntry:
|
|
@@ -64,6 +73,7 @@ class _ClarifyEntry:
|
|
|
64
73
|
self.data = data
|
|
65
74
|
self.event = threading.Event()
|
|
66
75
|
self.result: str | None = None
|
|
76
|
+
self.deadline: float | None = None
|
|
67
77
|
|
|
68
78
|
|
|
69
79
|
_RUNS: dict[str, _Run] = {}
|
|
@@ -79,7 +89,7 @@ _AGENT_CACHE_LOCK = threading.Lock()
|
|
|
79
89
|
# Running release thread -> the profile home it runs under.
|
|
80
90
|
_RELEASES: dict[threading.Thread, Any] = {}
|
|
81
91
|
_RELEASES_LOCK = threading.Lock()
|
|
82
|
-
# Agents inside ``chat.commit_memory`` (guarded by ``_AGENT_CACHE_LOCK``): like a turn,
|
|
92
|
+
# Agents inside ``chat.commit_memory`` or ``chat.refine`` (guarded by ``_AGENT_CACHE_LOCK``): like a turn, each defers their release.
|
|
83
93
|
_COMMITTING: list = []
|
|
84
94
|
# Set (under ``_RUNS_LOCK``) when shutdown drains: no turn is admitted after the drain's snapshot of ``_RUNS``.
|
|
85
95
|
_DRAINING = threading.Event()
|
|
@@ -114,6 +124,31 @@ def _result_diff(raw: Any) -> str | None:
|
|
|
114
124
|
return diff if isinstance(diff, str) else None
|
|
115
125
|
|
|
116
126
|
|
|
127
|
+
def _edit_snapshot(name: str, args: Any, task_id: str) -> Any:
|
|
128
|
+
"""TAL-909: the Agent's before-state of a file edit, through the task's terminal; None for a remote backend (the
|
|
129
|
+
Agent never diffs an unrelated local file), a non-edit tool, or any failure."""
|
|
130
|
+
try:
|
|
131
|
+
from agent.display import capture_local_edit_snapshot
|
|
132
|
+
|
|
133
|
+
return capture_local_edit_snapshot(name, args, task_id=task_id)
|
|
134
|
+
except Exception: # noqa: BLE001
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _edit_metadata(name: str, args: Any, result: Any, snapshot: Any) -> dict:
|
|
139
|
+
"""TAL-909: a tool-result row's ``display_metadata``: an edit's unified ``diff`` from its snapshot, which the server
|
|
140
|
+
turns into ``edit_diff``. Empty when the result carries its own ``diff`` (``patch``), which the server already reads."""
|
|
141
|
+
if snapshot is None or _result_diff(result) is not None:
|
|
142
|
+
return {}
|
|
143
|
+
try:
|
|
144
|
+
from agent.display import extract_edit_diff
|
|
145
|
+
|
|
146
|
+
diff = extract_edit_diff(name, result, function_args=args, snapshot=snapshot)
|
|
147
|
+
except Exception: # noqa: BLE001
|
|
148
|
+
return {}
|
|
149
|
+
return {"diff": diff} if diff else {}
|
|
150
|
+
|
|
151
|
+
|
|
117
152
|
def _raw_result(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> Any:
|
|
118
153
|
"""The tool result as the server's outcome rule reads it, bounded: a dict (or JSON-object text) keeps its first
|
|
119
154
|
``_RAW_RESULT_MAX_KEYS`` top-level fields plus its outcome fields, scalars as they are, text and non-empty nested
|
|
@@ -138,6 +173,18 @@ def _raw_result(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> Any:
|
|
|
138
173
|
return out
|
|
139
174
|
|
|
140
175
|
|
|
176
|
+
_TODO_TOOL_NAMES = frozenset({"todo", "todo_list"})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _is_todo_tool(name: Any) -> bool:
|
|
180
|
+
"""TAL-889: the Agent's own todo-tool predicate (canonical ``todo_list`` and legacy aliases); older Agents lack it."""
|
|
181
|
+
try:
|
|
182
|
+
from tools.todo_tool import is_todo_tool_name
|
|
183
|
+
except Exception: # noqa: BLE001 - older Agent without the predicate
|
|
184
|
+
return isinstance(name, str) and name in _TODO_TOOL_NAMES
|
|
185
|
+
return bool(is_todo_tool_name(name))
|
|
186
|
+
|
|
187
|
+
|
|
141
188
|
def _delegation_cost_usd(name: Any, raw: Any):
|
|
142
189
|
"""Predecessor ``_delegation_cost_usd``: total spend a ``delegate_task`` result reports, or None."""
|
|
143
190
|
if str(name or "") != "delegate_task":
|
|
@@ -255,9 +302,29 @@ def _context_length(agent: Any) -> dict:
|
|
|
255
302
|
value = getattr(cc, key, None)
|
|
256
303
|
if isinstance(value, (int, float)):
|
|
257
304
|
out[key] = int(value)
|
|
305
|
+
threshold = _next_turn_threshold(agent, cc)
|
|
306
|
+
if threshold:
|
|
307
|
+
out["next_turn_threshold_tokens"] = threshold
|
|
258
308
|
return out
|
|
259
309
|
|
|
260
310
|
|
|
311
|
+
def _next_turn_threshold(agent: Any, cc: Any) -> int | None:
|
|
312
|
+
"""TAL-1005: the compression threshold the next turn runs under. A turn that ended on a fallback leaves the
|
|
313
|
+
fallback's threshold on the compressor, but the Agent restores the primary runtime at the next turn's start
|
|
314
|
+
(``restore_primary_runtime``), so this is the trigger the compressor installs for that runtime's snapshot."""
|
|
315
|
+
value = getattr(cc, "threshold_tokens", None)
|
|
316
|
+
primary = getattr(agent, "_primary_runtime", None)
|
|
317
|
+
if getattr(agent, "_fallback_activated", False) is True and isinstance(primary, dict):
|
|
318
|
+
value = primary.get("compressor_threshold_tokens")
|
|
319
|
+
preview = getattr(cc, "preview_threshold_tokens", None)
|
|
320
|
+
if callable(preview) and isinstance(primary.get("compressor_context_length"), int):
|
|
321
|
+
try:
|
|
322
|
+
value = preview(str(primary.get("compressor_model") or ""), primary["compressor_context_length"], str(primary.get("compressor_provider") or ""))
|
|
323
|
+
except Exception: # noqa: BLE001
|
|
324
|
+
log.debug("primary threshold preview failed", exc_info=True)
|
|
325
|
+
return int(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0 else None
|
|
326
|
+
|
|
327
|
+
|
|
261
328
|
# TAL-539: the hard cap on raw tool output kept in a compressed context (predecessor ``_hard_prune_post_compression_*``).
|
|
262
329
|
_TAIL_TOOL_BUDGET_TOKENS = 4096
|
|
263
330
|
_TAIL_TOOL_MIN_SNIPPET_TOKENS = 256
|
|
@@ -280,25 +347,111 @@ def _rough_tokens(text: str) -> int:
|
|
|
280
347
|
return -(-len(text) // _ROUGH_TOKEN_CHARS)
|
|
281
348
|
|
|
282
349
|
|
|
350
|
+
def _tool_failure_outcome(text: str) -> str:
|
|
351
|
+
"""`` FAILED: <error>`` for a JSON tool payload with a top-level ``error`` / ``success: false``, `` FAILED: exit N`` for
|
|
352
|
+
a non-zero ``exit_code`` / ``exitCode`` the agent did not kill itself, else ``""``: the Agent's compression-stub rule
|
|
353
|
+
(``_payload_failure_suffix``, ``_sum_process_manage``). Payloads put these fields after ``output``, so a cut drops them."""
|
|
354
|
+
try:
|
|
355
|
+
payload = json.loads(text)
|
|
356
|
+
except (ValueError, RecursionError):
|
|
357
|
+
return ""
|
|
358
|
+
if not isinstance(payload, dict):
|
|
359
|
+
return ""
|
|
360
|
+
error = payload.get("error")
|
|
361
|
+
if not error and payload.get("success") is not False:
|
|
362
|
+
code = payload.get("exit_code")
|
|
363
|
+
code = payload.get("exitCode") if code is None else code
|
|
364
|
+
if not isinstance(code, int) or isinstance(code, bool) or code == 0 or payload.get("completion_reason") == "killed":
|
|
365
|
+
return ""
|
|
366
|
+
error = f"exit {code}"
|
|
367
|
+
preview = " ".join(str(error).split())[:80] if error else ""
|
|
368
|
+
return f" FAILED: {preview}" if preview else " FAILED"
|
|
369
|
+
|
|
370
|
+
|
|
283
371
|
def _capped_tool_result(text: str, tokens: int, keep_tokens: int) -> str:
|
|
284
|
-
"""The kept head of an over-budget tool result plus a note on what the model no longer sees.
|
|
285
|
-
|
|
286
|
-
|
|
372
|
+
"""The kept head of an over-budget tool result plus a note on what the model no longer sees, ending on the outcome.
|
|
373
|
+
TAL-827: the Agent's elision marker marks the cut, so its dispatch guard rejects a model that copies the marker
|
|
374
|
+
into a durable write; Talaria's note follows on its own line."""
|
|
375
|
+
outcome = _tool_failure_outcome(text)
|
|
376
|
+
try:
|
|
377
|
+
from agent.compression_marker import elide_middle
|
|
378
|
+
except ImportError: # An Agent without the shared marker keeps Talaria's own.
|
|
379
|
+
prefix = "[Talaria context budget: omitted "
|
|
380
|
+
suffix = f" (~{tokens} rough tokens) from this tool result; the full output remains in the visible transcript.{outcome}]"
|
|
381
|
+
note_only, widest = f"{prefix}{len(text)} chars{suffix}", f"{prefix}{len(text)} of {len(text)} chars{suffix}"
|
|
382
|
+
|
|
383
|
+
def cut(snippet: str) -> str:
|
|
384
|
+
return f"{snippet}\n\n{prefix}{len(text) - len(snippet)} of {len(text)} chars{suffix}"
|
|
385
|
+
else:
|
|
386
|
+
note = f"\n[Talaria context budget: this tool result is ~{tokens} rough tokens; the full output remains in the visible transcript.{outcome}]"
|
|
387
|
+
# With nothing kept the marker counts every char, its widest form for this text.
|
|
388
|
+
note_only = widest = elide_middle(text, 0, 0) + note
|
|
389
|
+
|
|
390
|
+
def cut(snippet: str) -> str:
|
|
391
|
+
return elide_middle(text, len(snippet), 0) + note
|
|
287
392
|
if keep_tokens < _TAIL_TOOL_MIN_SNIPPET_TOKENS:
|
|
288
|
-
return
|
|
289
|
-
room = keep_tokens - _rough_tokens(
|
|
393
|
+
return note_only
|
|
394
|
+
room = keep_tokens - _rough_tokens(widest) - 1
|
|
290
395
|
snippet = text[: len(text) * room // tokens]
|
|
291
396
|
# Density varies along the text: shrink until the kept head fits the remaining tokens.
|
|
292
397
|
while snippet and _rough_tokens(snippet) > room:
|
|
293
398
|
snippet = snippet[: len(snippet) * 3 // 4]
|
|
294
399
|
snippet = snippet.rstrip()
|
|
295
|
-
if
|
|
296
|
-
return f"{prefix}{len(text)} chars{suffix}"
|
|
297
|
-
return f"{snippet}\n\n{prefix}{len(text) - len(snippet)} of {len(text)} chars{suffix}"
|
|
400
|
+
return cut(snippet) if snippet else note_only
|
|
298
401
|
|
|
299
402
|
|
|
300
|
-
def
|
|
301
|
-
"""
|
|
403
|
+
def _skill_result_stubs(messages: list) -> dict[int, str]:
|
|
404
|
+
"""Recovery metadata for paired skill loads, captured before the Agent can prune their success/failure payloads.
|
|
405
|
+
Extends the Agent's call-id and marker helpers; its older summaries omit small-skill markers and failure verdicts."""
|
|
406
|
+
try:
|
|
407
|
+
from agent.context_compressor import _tool_calls_by_id
|
|
408
|
+
except ImportError:
|
|
409
|
+
return {}
|
|
410
|
+
try:
|
|
411
|
+
from agent.context_compressor import _skill_pruned_marker
|
|
412
|
+
except ImportError:
|
|
413
|
+
def _skill_pruned_marker(name: str) -> str:
|
|
414
|
+
return f"[SKILL_PRUNED: content lost in compression; reload with skill_view(name='{name}')]"
|
|
415
|
+
|
|
416
|
+
calls = _tool_calls_by_id(messages)
|
|
417
|
+
stubs = {}
|
|
418
|
+
for i, msg in enumerate(messages):
|
|
419
|
+
if msg.get("role") != "tool" or not msg.get("tool_call_id"):
|
|
420
|
+
continue
|
|
421
|
+
tool, raw_args = calls.get(msg["tool_call_id"], (None, None))
|
|
422
|
+
if tool != "skill_view":
|
|
423
|
+
continue
|
|
424
|
+
try:
|
|
425
|
+
args = json.loads(raw_args)
|
|
426
|
+
except (TypeError, ValueError, RecursionError):
|
|
427
|
+
continue
|
|
428
|
+
name = args.get("name") if isinstance(args, dict) else None
|
|
429
|
+
if not isinstance(name, str) or not name or args.get("file_path"):
|
|
430
|
+
continue
|
|
431
|
+
text = _content_text(msg.get("content"))
|
|
432
|
+
outcome = _tool_failure_outcome(text)
|
|
433
|
+
if text.startswith("[skill_view] ") and " FAILED" in text.split(" (", 1)[0]:
|
|
434
|
+
outcome = " FAILED"
|
|
435
|
+
if outcome:
|
|
436
|
+
stubs[i] = f"[skill_view] name={name}{outcome}"
|
|
437
|
+
continue
|
|
438
|
+
try:
|
|
439
|
+
payload = json.loads(text)
|
|
440
|
+
except (ValueError, RecursionError):
|
|
441
|
+
payload = None
|
|
442
|
+
successful = isinstance(payload, dict) and payload.get("success") is True and isinstance(payload.get("content"), str)
|
|
443
|
+
marker = _skill_pruned_marker(name)
|
|
444
|
+
# Lean-tail stubs predate the current cap and may have been persisted by a different supported Agent.
|
|
445
|
+
demoted = text.startswith(("[skill_view output demoted at compaction ", "[tool output demoted at compaction ")) and text.endswith("] " + marker)
|
|
446
|
+
if successful or text.startswith(f"[skill_view] name={name} (") or text == marker or demoted:
|
|
447
|
+
stubs[i] = marker
|
|
448
|
+
return stubs
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _cap_tool_results(messages: list, compressor: Any, original_messages: list | None = None) -> list:
|
|
452
|
+
"""Newest bodies spend one shared budget. Skill reload/failure stubs, like omission notes, survive exhaustion."""
|
|
453
|
+
originals = messages if original_messages is None else original_messages
|
|
454
|
+
skill_stubs = _skill_result_stubs(originals)
|
|
302
455
|
budget = getattr(compressor, "tail_token_budget", None)
|
|
303
456
|
budget = budget if isinstance(budget, int) and budget > 0 else _TAIL_TOOL_BUDGET_TOKENS
|
|
304
457
|
threshold = getattr(compressor, "threshold_tokens", None)
|
|
@@ -312,13 +465,17 @@ def _cap_tool_results(messages: list, compressor: Any) -> list:
|
|
|
312
465
|
if msg.get("role") != "tool":
|
|
313
466
|
continue
|
|
314
467
|
text = _content_text(msg.get("content"))
|
|
468
|
+
stub = skill_stubs.get(i)
|
|
469
|
+
if stub and text != _content_text(originals[i].get("content")):
|
|
470
|
+
text = stub
|
|
471
|
+
out[i] = {**msg, "content": text}
|
|
315
472
|
if not text.strip():
|
|
316
473
|
continue
|
|
317
474
|
tokens = _rough_tokens(text)
|
|
318
475
|
if tokens <= budget - spent:
|
|
319
476
|
spent += tokens
|
|
320
477
|
continue
|
|
321
|
-
out[i] = {**msg, "content": _capped_tool_result(text, tokens, budget - spent)}
|
|
478
|
+
out[i] = {**msg, "content": stub or _capped_tool_result(text, tokens, budget - spent)}
|
|
322
479
|
spent = budget
|
|
323
480
|
return out
|
|
324
481
|
|
|
@@ -337,7 +494,7 @@ def _post_compression_context(agent: Any, messages: list, system_message: Any) -
|
|
|
337
494
|
context = pruned
|
|
338
495
|
except Exception: # noqa: BLE001 - the hard cap below still bounds the context
|
|
339
496
|
log.debug("post-compression tool-result pruning failed", exc_info=True)
|
|
340
|
-
context = _cap_tool_results(context, compressor)
|
|
497
|
+
context = _cap_tool_results(context, compressor, messages)
|
|
341
498
|
estimate = None
|
|
342
499
|
try:
|
|
343
500
|
from agent.model_metadata import estimate_request_tokens_rough
|
|
@@ -350,6 +507,40 @@ def _post_compression_context(agent: Any, messages: list, system_message: Any) -
|
|
|
350
507
|
return context, estimate if isinstance(estimate, int) and estimate > 0 else None
|
|
351
508
|
|
|
352
509
|
|
|
510
|
+
# TAL-887: the Agent's password-manager prompts, answered like a clarify; the waits are the Agent's own surfaces' (its TUI).
|
|
511
|
+
_VAULT_PROMPT_TIMEOUTS = {"vault_unlock": 120, "vault_save_login": 180, "vault_code": 180, "skill_secret": 120}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _set_vault_session(session_id) -> None:
|
|
515
|
+
"""Bind the session an unlock made on this thread belongs to (released with it)."""
|
|
516
|
+
try:
|
|
517
|
+
from agent.vault_backends import unlock
|
|
518
|
+
except Exception: # noqa: BLE001 - an Agent without the credential vault
|
|
519
|
+
return
|
|
520
|
+
unlock.set_current_session_id(session_id)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _release_vault_session(session_id) -> None:
|
|
524
|
+
"""Lock the password managers ``session_id`` unlocked; other sessions keep theirs."""
|
|
525
|
+
try:
|
|
526
|
+
from agent.vault_backends.unlock import release_session
|
|
527
|
+
except Exception: # noqa: BLE001 - an Agent without the credential vault
|
|
528
|
+
return
|
|
529
|
+
release_session(session_id)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _set_vault_prompts(session_id, unlock_cb, save_login_cb, code_cb) -> None:
|
|
533
|
+
"""Bind (or with ``None``s clear) the Agent's per-thread vault prompts and the session an unlock belongs to."""
|
|
534
|
+
try:
|
|
535
|
+
from agent.vault_backends import unlock
|
|
536
|
+
except Exception: # noqa: BLE001 - an Agent without the credential vault
|
|
537
|
+
return
|
|
538
|
+
_set_vault_session(session_id)
|
|
539
|
+
unlock.set_unlock_prompt_callback(unlock_cb)
|
|
540
|
+
unlock.set_save_login_prompt_callback(save_login_cb)
|
|
541
|
+
unlock.set_code_prompt_callback(code_cb)
|
|
542
|
+
|
|
543
|
+
|
|
353
544
|
def _clarify_timeout(params: dict) -> int:
|
|
354
545
|
"""Predecessor ``_clarify_timeout_seconds``: an explicit request value wins, else the Agent's resolver over config."""
|
|
355
546
|
explicit = params.get("clarify_timeout_seconds")
|
|
@@ -481,7 +672,7 @@ def _resolve_runtime(provider: str | None, model: str) -> dict:
|
|
|
481
672
|
try:
|
|
482
673
|
runtime = resolve_runtime_provider(requested=provider or None, target_model=model or None)
|
|
483
674
|
except Exception as exc: # noqa: BLE001
|
|
484
|
-
raise
|
|
675
|
+
raise runtime_failure(exc, provider) from exc
|
|
485
676
|
if not isinstance(runtime, dict):
|
|
486
677
|
runtime = dict(vars(runtime)) if hasattr(runtime, "__dict__") else {}
|
|
487
678
|
return runtime
|
|
@@ -520,14 +711,8 @@ def _turn_identity(session_id: str, workspace: str):
|
|
|
520
711
|
except Exception: # noqa: BLE001
|
|
521
712
|
log.debug("per-turn session context bind failed", exc_info=True)
|
|
522
713
|
try:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
tok = _SESSION_CWD.set(str(workspace))
|
|
526
|
-
resets.append(lambda: _SESSION_CWD.reset(tok))
|
|
527
|
-
except Exception: # noqa: BLE001
|
|
528
|
-
log.debug("per-turn session cwd bind failed", exc_info=True)
|
|
529
|
-
try:
|
|
530
|
-
yield
|
|
714
|
+
with session_workspace(workspace):
|
|
715
|
+
yield
|
|
531
716
|
finally:
|
|
532
717
|
for reset in reversed(resets):
|
|
533
718
|
try:
|
|
@@ -562,16 +747,13 @@ def _discover_mcp_tools(run: _Run) -> None:
|
|
|
562
747
|
The Agent keys connections by profile scope and skips live ones, so later turns are cheap. Runs under the call's
|
|
563
748
|
``scoped_home``; a failure leaves the turn without MCP tools rather than failing it. A Stop does not wait for a slow
|
|
564
749
|
server: the turn moves on to its cancelled end while the connect finishes in the background."""
|
|
565
|
-
from .commands import
|
|
750
|
+
from .commands import discover_profile_mcp_tools
|
|
566
751
|
|
|
567
752
|
done = threading.Event()
|
|
568
753
|
|
|
569
754
|
def discover() -> None:
|
|
570
755
|
try:
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
retire_unscoped_launch_mcp_servers()
|
|
574
|
-
discover_mcp_tools()
|
|
756
|
+
discover_profile_mcp_tools()
|
|
575
757
|
except Exception: # noqa: BLE001
|
|
576
758
|
log.warning("MCP discovery failed", exc_info=True)
|
|
577
759
|
finally:
|
|
@@ -593,7 +775,18 @@ def _profile_fallback_chain() -> list[dict] | None:
|
|
|
593
775
|
return get_fallback_chain(load_config() or {}) or None
|
|
594
776
|
|
|
595
777
|
|
|
596
|
-
def
|
|
778
|
+
def _profile_prefill_messages() -> list[dict] | None:
|
|
779
|
+
"""The profile's few-shot prefill (``HERMES_PREFILL_MESSAGES_FILE``, then ``prefill_messages_file``, then legacy
|
|
780
|
+
``agent.prefill_messages_file``; relative to the profile) through the Agent's own loader, like the CLI, gateway and
|
|
781
|
+
Desktop. A missing or empty file is None. Runs under the call's ``scoped_home``."""
|
|
782
|
+
from hermes_cli.cli_config_load import _load_prefill_messages, _resolve_prefill_messages_file
|
|
783
|
+
from hermes_cli.config import load_config
|
|
784
|
+
from hermes_constants import get_hermes_home
|
|
785
|
+
|
|
786
|
+
return _load_prefill_messages(_resolve_prefill_messages_file(load_config() or {}), get_hermes_home()) or None
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _main_model_request_overrides(model: str, provider, base_url: str = "") -> dict | None:
|
|
597
790
|
"""Predecessor ``_main_model_request_overrides``: the profile's ``model.service_tier`` and ``model.extra_body``
|
|
598
791
|
(written by ``/api/model/set``) apply only to turns on that configured main model. Runs under ``scoped_home``."""
|
|
599
792
|
from hermes_cli.config import load_config
|
|
@@ -606,17 +799,31 @@ def _main_model_request_overrides(model: str, provider) -> dict | None:
|
|
|
606
799
|
if not main_model or main_model != model.strip():
|
|
607
800
|
return None
|
|
608
801
|
# An unset or ``auto`` main provider resolves at runtime, so the model id alone identifies the main model.
|
|
609
|
-
|
|
802
|
+
from hermes_cli.models import normalize_provider
|
|
803
|
+
|
|
804
|
+
if main_provider not in ("", "auto") and normalize_provider(main_provider) != normalize_provider(str(provider or "").strip().lower()):
|
|
610
805
|
return None
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
806
|
+
from ..speed import fast_overrides
|
|
807
|
+
|
|
808
|
+
overrides = fast_overrides(model, str(provider or ""), base_url, str(model_cfg.get("service_tier") or "").strip().lower()) or {}
|
|
614
809
|
extra_body = model_cfg.get("extra_body")
|
|
615
810
|
if isinstance(extra_body, dict) and extra_body:
|
|
616
811
|
overrides["extra_body"] = extra_body
|
|
617
812
|
return overrides or None
|
|
618
813
|
|
|
619
814
|
|
|
815
|
+
def _request_overrides(model: str, provider, runtime: dict) -> dict | None:
|
|
816
|
+
"""The resolver's ``request_overrides`` (a custom provider entry's ``extra_body``) under the main-model ones, whose
|
|
817
|
+
``extra_body`` wins key by key, the Agent's own merge order. Runs under ``scoped_home``."""
|
|
818
|
+
merged = dict(runtime.get("request_overrides") or {})
|
|
819
|
+
main = _main_model_request_overrides(model, provider, str(runtime.get("base_url") or "")) or {}
|
|
820
|
+
extra_body = {**(merged.get("extra_body") or {}), **(main.get("extra_body") or {})}
|
|
821
|
+
merged.update(main)
|
|
822
|
+
if extra_body:
|
|
823
|
+
merged["extra_body"] = extra_body
|
|
824
|
+
return merged or None
|
|
825
|
+
|
|
826
|
+
|
|
620
827
|
def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, kwargs: dict) -> str:
|
|
621
828
|
"""Cache identity of an ``AIAgent``: everything its constructor bound from the resolved runtime, so a rotated key,
|
|
622
829
|
a different API mode, ACP command, or credential pool never reuses an agent built for the old bundle. The key
|
|
@@ -628,7 +835,8 @@ def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, k
|
|
|
628
835
|
bundle = {
|
|
629
836
|
"model": model, "provider": provider, "base_url": runtime.get("base_url"), "api_key": key_digest,
|
|
630
837
|
"api_mode": runtime.get("api_mode"), "acp_command": runtime.get("acp_command"), "acp_args": runtime.get("acp_args"),
|
|
631
|
-
"credential_pool": pool_identity, "toolsets": toolsets, "home": home,
|
|
838
|
+
"credential_pool": pool_identity, "toolsets": toolsets, "home": home, "cwd": kwargs.get("cwd"),
|
|
839
|
+
"disabled_toolsets": kwargs.get("disabled_toolsets"),
|
|
632
840
|
"max_iterations": kwargs.get("max_iterations"), "max_tokens": kwargs.get("max_tokens"),
|
|
633
841
|
# Bound at construction too: a reasoning-effort change from the composer must build a fresh agent.
|
|
634
842
|
"reasoning_config": kwargs.get("reasoning_config"),
|
|
@@ -636,10 +844,79 @@ def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, k
|
|
|
636
844
|
"checkpoints": [kwargs.get(name) for name in ("checkpoints_enabled", "checkpoint_max_snapshots", "checkpoint_max_total_size_mb", "checkpoint_max_file_size_mb")],
|
|
637
845
|
# Entries may carry their own ``api_key``.
|
|
638
846
|
"fallback_model": hashlib.sha256(json.dumps(kwargs.get("fallback_model"), sort_keys=True, default=str).encode("utf-8")).hexdigest()[:16],
|
|
847
|
+
# Read each turn, so an edited prefill file builds a fresh agent.
|
|
848
|
+
"prefill_messages": hashlib.sha256(json.dumps(kwargs.get("prefill_messages"), sort_keys=True, default=str).encode("utf-8")).hexdigest()[:16],
|
|
639
849
|
}
|
|
640
850
|
return json.dumps(bundle, sort_keys=True, default=str)
|
|
641
851
|
|
|
642
852
|
|
|
853
|
+
def _agent_kwargs(AIAgent, params: dict, session_id: str, model: str, provider, runtime: dict, toolsets) -> dict:
|
|
854
|
+
"""The ``AIAgent`` arguments of a session's agent apart from a turn's callbacks: the resolved runtime, the session's
|
|
855
|
+
toolsets and budgets, and the profile's fallback chain, request overrides, prefill and checkpoints. Everything
|
|
856
|
+
``_agent_signature`` reads is here, so an agent ``/refine`` builds (TAL-902) is the one the next turn reuses. Runs under
|
|
857
|
+
the call's ``scoped_home``."""
|
|
858
|
+
kwargs: dict = dict(
|
|
859
|
+
model=model,
|
|
860
|
+
provider=provider,
|
|
861
|
+
base_url=runtime.get("base_url"),
|
|
862
|
+
api_key=runtime.get("api_key"),
|
|
863
|
+
platform="webui",
|
|
864
|
+
quiet_mode=True,
|
|
865
|
+
enabled_toolsets=toolsets,
|
|
866
|
+
disabled_toolsets=disabled_toolsets(),
|
|
867
|
+
session_id=session_id,
|
|
868
|
+
)
|
|
869
|
+
for name, value in (
|
|
870
|
+
("api_mode", runtime.get("api_mode")),
|
|
871
|
+
("acp_command", runtime.get("acp_command")),
|
|
872
|
+
("acp_args", runtime.get("acp_args")),
|
|
873
|
+
("credential_pool", runtime.get("credential_pool")),
|
|
874
|
+
("gateway_session_key", session_id),
|
|
875
|
+
# TAL-903: anchors subdirectory AGENTS.md discovery (and the memory provider) at the session workspace, when the
|
|
876
|
+
# server says it is a host path; a remote terminal backend's target-side path must never be read on the host.
|
|
877
|
+
("cwd", params.get("workspace") if params.get("workspace_local") is True else None),
|
|
878
|
+
):
|
|
879
|
+
if _supported(AIAgent, name) and value is not None:
|
|
880
|
+
kwargs[name] = value
|
|
881
|
+
if isinstance(params.get("max_iterations"), int) and params["max_iterations"] > 0 and _supported(AIAgent, "max_iterations"):
|
|
882
|
+
kwargs["max_iterations"] = params["max_iterations"]
|
|
883
|
+
if isinstance(params.get("max_tokens"), int) and params["max_tokens"] > 0 and _supported(AIAgent, "max_tokens"):
|
|
884
|
+
kwargs["max_tokens"] = params["max_tokens"]
|
|
885
|
+
# Predecessor: `agent.reasoning_effort` (already coerced for the model) → AIAgent ``reasoning_config``.
|
|
886
|
+
reasoning_config = params.get("reasoning_config")
|
|
887
|
+
if isinstance(reasoning_config, dict) and _supported(AIAgent, "reasoning_config"):
|
|
888
|
+
kwargs["reasoning_config"] = reasoning_config
|
|
889
|
+
fallback_chain = _profile_fallback_chain()
|
|
890
|
+
if fallback_chain and _supported(AIAgent, "fallback_model"):
|
|
891
|
+
kwargs["fallback_model"] = fallback_chain
|
|
892
|
+
request_overrides = _request_overrides(model, provider, runtime)
|
|
893
|
+
if request_overrides and _supported(AIAgent, "request_overrides"):
|
|
894
|
+
kwargs["request_overrides"] = request_overrides
|
|
895
|
+
prefill_messages = _profile_prefill_messages()
|
|
896
|
+
if prefill_messages and _supported(AIAgent, "prefill_messages"):
|
|
897
|
+
kwargs["prefill_messages"] = prefill_messages
|
|
898
|
+
# TAL-913: the profile's ``checkpoints`` section, as the Agent's own CLI and gateway turns read it.
|
|
899
|
+
for name, value in checkpoint_kwargs().items():
|
|
900
|
+
if _supported(AIAgent, name):
|
|
901
|
+
kwargs[name] = value
|
|
902
|
+
return kwargs
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _build_agent(AIAgent, kwargs: dict, home, session_id: str, state_db: bool = True):
|
|
906
|
+
"""A new session agent on the profile's ``state.db`` (unless ``state_db`` is false), which it releases with it."""
|
|
907
|
+
session_db = _acquire_session_db(home) if state_db and _supported(AIAgent, "session_db") else None
|
|
908
|
+
if session_db is not None:
|
|
909
|
+
kwargs["session_db"] = session_db
|
|
910
|
+
kwargs["session_id"] = _execution_session_id(session_db, session_id)
|
|
911
|
+
try:
|
|
912
|
+
agent = AIAgent(**kwargs)
|
|
913
|
+
except BaseException:
|
|
914
|
+
_release_session_db(session_db)
|
|
915
|
+
raise
|
|
916
|
+
agent._talaria_session_db = session_db # released with the agent (``_release_agent``)
|
|
917
|
+
return agent
|
|
918
|
+
|
|
919
|
+
|
|
643
920
|
def _evict_idle_agents_locked() -> None:
|
|
644
921
|
"""Trim the agent LRU (caller holds ``_AGENT_CACHE_LOCK``), never dropping a session whose turn is still running."""
|
|
645
922
|
if len(_AGENT_CACHE) <= _AGENT_CACHE_MAX:
|
|
@@ -718,6 +995,73 @@ def _join(threads, deadline: float) -> None:
|
|
|
718
995
|
thread.join(max(0.0, deadline - time.monotonic()))
|
|
719
996
|
|
|
720
997
|
|
|
998
|
+
def _cancel_background_reviews(entries, message: str) -> list[threading.Thread]:
|
|
999
|
+
"""TAL-897: stop the post-turn memory/skill review fork of each ``(agent, home)``, which no turn interrupt or release
|
|
1000
|
+
reaches, so a deleted session or a stopping sidecar no longer pays for it. Each cancel then holds the agent's review
|
|
1001
|
+
slot (a run token from the Agent's ``prepare_background_review_run``, never finished), so the Agent admits no later
|
|
1002
|
+
review: not from a turn still running (a clear does not stop it), a spawn already under way, or a review deferred to
|
|
1003
|
+
or requeued on the idle queue (managed local runtime). ``skip_background_review`` spares the turn finalizer queueing
|
|
1004
|
+
one at all. Only a turn's start waits on a held slot, and an ended agent runs no further turn.
|
|
1005
|
+
|
|
1006
|
+
A cancel lasts until its fork acknowledges, which the Agent's own wait does not bound, and runs as a release under
|
|
1007
|
+
``home`` (``_RELEASES``): shutdown waits for it, and profile deletion, retries included, refuses while it runs or
|
|
1008
|
+
once it failed."""
|
|
1009
|
+
unique = {id(agent): (agent, home) for agent, home in entries if agent is not None}
|
|
1010
|
+
for agent, _home in unique.values():
|
|
1011
|
+
try:
|
|
1012
|
+
agent.skip_background_review = True
|
|
1013
|
+
except AttributeError: # an object without instance attributes forks no reviews
|
|
1014
|
+
pass
|
|
1015
|
+
try:
|
|
1016
|
+
from agent.background_review import cancel_background_review_for_live_turn, prepare_background_review_run
|
|
1017
|
+
except ImportError:
|
|
1018
|
+
return []
|
|
1019
|
+
|
|
1020
|
+
def cancel(agent) -> None:
|
|
1021
|
+
thread = threading.current_thread()
|
|
1022
|
+
try:
|
|
1023
|
+
try:
|
|
1024
|
+
# One cancel per agent at a time, so none mistakes another's freshly held slot for a review to cancel.
|
|
1025
|
+
lock = vars(agent).setdefault("_talaria_review_cancel_lock", threading.Lock())
|
|
1026
|
+
except TypeError:
|
|
1027
|
+
return # no instance attributes, so no review slot either
|
|
1028
|
+
with lock:
|
|
1029
|
+
# A spawn that slipped in between the cancel and the slot claim is cancelled on the next pass.
|
|
1030
|
+
for _ in range(3):
|
|
1031
|
+
if getattr(agent, "_talaria_review_slot", None) is not None:
|
|
1032
|
+
return
|
|
1033
|
+
cancel_background_review_for_live_turn(agent, message=message, tool_reason="session ended")
|
|
1034
|
+
slot = prepare_background_review_run(agent)
|
|
1035
|
+
if slot is not None:
|
|
1036
|
+
agent._talaria_review_slot = slot
|
|
1037
|
+
return
|
|
1038
|
+
current = getattr(agent, "_background_review_run", None)
|
|
1039
|
+
if current is None:
|
|
1040
|
+
return # not an Agent with a review slot
|
|
1041
|
+
if current.cancel_requested.is_set():
|
|
1042
|
+
# A cancelled fork past the Agent's wait may still write: wait for it to acknowledge, then claim
|
|
1043
|
+
# the slot before the Agent can requeue it.
|
|
1044
|
+
current.request_done.wait()
|
|
1045
|
+
log.warning("a background review kept restarting after its session ended")
|
|
1046
|
+
thread.failed = True
|
|
1047
|
+
except Exception: # noqa: BLE001 - teardown continues without it
|
|
1048
|
+
log.warning("cancelling a background review failed", exc_info=True)
|
|
1049
|
+
thread.failed = True
|
|
1050
|
+
finally:
|
|
1051
|
+
with _RELEASES_LOCK:
|
|
1052
|
+
_RELEASES.pop(thread, None)
|
|
1053
|
+
|
|
1054
|
+
threads = []
|
|
1055
|
+
for agent, home in unique.values():
|
|
1056
|
+
thread = threading.Thread(target=cancel, args=(agent,), daemon=True, name="chat-review-cancel")
|
|
1057
|
+
thread.failed = False # type: ignore[attr-defined] - read by profile deletion
|
|
1058
|
+
with _RELEASES_LOCK:
|
|
1059
|
+
_RELEASES[thread] = home
|
|
1060
|
+
thread.start()
|
|
1061
|
+
threads.append(thread)
|
|
1062
|
+
return threads
|
|
1063
|
+
|
|
1064
|
+
|
|
721
1065
|
def evict_all_agents() -> int:
|
|
722
1066
|
"""Drop every cached agent (credentials or environment changed underneath them)."""
|
|
723
1067
|
with _AGENT_CACHE_LOCK:
|
|
@@ -731,10 +1075,15 @@ def drain_agents(timeout: float = _AGENT_RELEASE_TIMEOUT) -> None:
|
|
|
731
1075
|
"""Shutdown: release every cached agent, stop running turns (like a Stop) so each releases the agent it holds, and
|
|
732
1076
|
wait, bounded, for those turns and every release still running."""
|
|
733
1077
|
deadline = time.monotonic() + timeout
|
|
734
|
-
|
|
735
|
-
|
|
1078
|
+
with _AGENT_CACHE_LOCK, _RUNS_LOCK:
|
|
1079
|
+
# Admission closes with the snapshot, so no later turn forks a review outside it.
|
|
736
1080
|
_DRAINING.set()
|
|
737
1081
|
runs = list(_RUNS.values())
|
|
1082
|
+
owners = [(entry[0], entry[2]) for entry in _AGENT_CACHE.values()] + [(run.agent, run.home) for run in runs]
|
|
1083
|
+
for run in runs:
|
|
1084
|
+
run.session_ended = True
|
|
1085
|
+
_cancel_background_reviews(owners, "sidecar shutting down")
|
|
1086
|
+
evict_all_agents()
|
|
738
1087
|
for run in runs:
|
|
739
1088
|
run.cancel.set()
|
|
740
1089
|
while time.monotonic() < deadline:
|
|
@@ -749,15 +1098,39 @@ def drain_agents(timeout: float = _AGENT_RELEASE_TIMEOUT) -> None:
|
|
|
749
1098
|
_join(threads, deadline)
|
|
750
1099
|
|
|
751
1100
|
|
|
1101
|
+
def refresh_profile_agents_mcp_tools(home) -> None:
|
|
1102
|
+
"""MCP reload: republish the live registry's tools to the profile's cached agents, like the gateway's
|
|
1103
|
+
``_mcp_reload_refresh_cached_agents``. ``content_aware`` publishes a same-name schema change on every supported
|
|
1104
|
+
Agent (0.22.0 implies it for an explicit reload, 0.21.x does not). Each agent keeps its build-time toolset
|
|
1105
|
+
selection. An agent whose refresh fails leaves the cache, so its session's next turn builds one from the live
|
|
1106
|
+
registry instead of keeping stale schemas; the rest still refresh."""
|
|
1107
|
+
from tools.mcp_tool_agent import refresh_agent_mcp_tools
|
|
1108
|
+
|
|
1109
|
+
target = os.path.realpath(home)
|
|
1110
|
+
with _AGENT_CACHE_LOCK:
|
|
1111
|
+
failed = []
|
|
1112
|
+
for session_id, entry in list(_AGENT_CACHE.items()):
|
|
1113
|
+
if os.path.realpath(entry[2]) != target:
|
|
1114
|
+
continue
|
|
1115
|
+
try:
|
|
1116
|
+
refresh_agent_mcp_tools(entry[0], quiet_mode=True, content_aware=True)
|
|
1117
|
+
except Exception: # noqa: BLE001
|
|
1118
|
+
log.warning("Failed to refresh MCP tools for session %s; evicting its agent", session_id, exc_info=True)
|
|
1119
|
+
failed.append(_AGENT_CACHE.pop(session_id))
|
|
1120
|
+
_release_agents_locked(failed)
|
|
1121
|
+
|
|
1122
|
+
|
|
752
1123
|
def release_profile_agents(home) -> None:
|
|
753
|
-
"""Profile deletion: release the profile's cached agents
|
|
754
|
-
rather than let the home go while a memory flush still runs under it."""
|
|
1124
|
+
"""Profile deletion: release the profile's cached agents, cancel their background reviews, and wait for both and
|
|
1125
|
+
every release running under its home. Fails rather than let the home go while a memory flush still runs under it."""
|
|
755
1126
|
target = os.path.realpath(home)
|
|
756
1127
|
with _AGENT_CACHE_LOCK:
|
|
757
1128
|
entries = [_AGENT_CACHE.pop(sid) for sid, entry in list(_AGENT_CACHE.items()) if os.path.realpath(entry[2]) == target]
|
|
758
|
-
_release_agents_locked(entries)
|
|
1129
|
+
started = _release_agents_locked(entries)
|
|
1130
|
+
started += _cancel_background_reviews([(entry[0], entry[2]) for entry in entries], "profile deleted")
|
|
1131
|
+
# The threads started here count even once they left ``_RELEASES``: a fast failure must still block the delete.
|
|
759
1132
|
with _RELEASES_LOCK:
|
|
760
|
-
threads = [thread for thread, owner in _RELEASES.items() if os.path.realpath(owner) == target]
|
|
1133
|
+
threads = list(dict.fromkeys(started + [thread for thread, owner in _RELEASES.items() if owner is not None and os.path.realpath(owner) == target]))
|
|
761
1134
|
_join(threads, time.monotonic() + _PROFILE_RELEASE_TIMEOUT)
|
|
762
1135
|
if any(thread.is_alive() for thread in threads):
|
|
763
1136
|
raise RpcError("The profile is still saving memory from its chats. Retry in a moment.")
|
|
@@ -823,6 +1196,14 @@ def _max_iterations_summary_request() -> str:
|
|
|
823
1196
|
return str(MAX_ITERATIONS_SUMMARY_REQUEST)
|
|
824
1197
|
|
|
825
1198
|
|
|
1199
|
+
def _has_voice_turn_route() -> bool:
|
|
1200
|
+
"""Whether the Agent routes a marked turn to ``auxiliary.voice_chat`` (``agent.voice_turn_route``, v0.21.6)."""
|
|
1201
|
+
try:
|
|
1202
|
+
return importlib.util.find_spec("agent.voice_turn_route") is not None
|
|
1203
|
+
except (ImportError, ValueError):
|
|
1204
|
+
return False
|
|
1205
|
+
|
|
1206
|
+
|
|
826
1207
|
def _supported(cls, name: str) -> bool:
|
|
827
1208
|
"""Whether ``cls.__init__`` accepts ``name`` (named, or through ``**kwargs``)."""
|
|
828
1209
|
try:
|
|
@@ -832,6 +1213,15 @@ def _supported(cls, name: str) -> bool:
|
|
|
832
1213
|
return name in parameters or any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values())
|
|
833
1214
|
|
|
834
1215
|
|
|
1216
|
+
@lru_cache(maxsize=32)
|
|
1217
|
+
def _run_conversation_parameters(cls) -> frozenset[str]:
|
|
1218
|
+
"""Probe explicit turn capabilities once per Agent class; unknown signatures keep legacy arguments."""
|
|
1219
|
+
try:
|
|
1220
|
+
return frozenset(inspect.signature(cls.run_conversation).parameters)
|
|
1221
|
+
except (TypeError, ValueError):
|
|
1222
|
+
return frozenset()
|
|
1223
|
+
|
|
1224
|
+
|
|
835
1225
|
def _approval_notifier(session_id: str, emit):
|
|
836
1226
|
"""The Agent's gateway notify callback for one turn: emits each parked approval, then ``approval_resolved``
|
|
837
1227
|
(id plus the Agent's reason) once its wait ends by any path, an ``approvals.timeout`` expiry included (TAL-514)."""
|
|
@@ -876,20 +1266,51 @@ def _is_compression_start(lower: str) -> bool:
|
|
|
876
1266
|
|
|
877
1267
|
|
|
878
1268
|
def _status_events(kind, text: str) -> list[str]:
|
|
879
|
-
"""Events for one Agent status line:
|
|
1269
|
+
"""Events for one Agent status line: compression, provider diagnostics, and the turn's terminal error."""
|
|
880
1270
|
lower = text.lower()
|
|
881
1271
|
events = ["terminal_error"] if "non-retryable error" in lower else []
|
|
882
|
-
|
|
1272
|
+
kind = str(kind or "").strip().lower()
|
|
1273
|
+
# TAL-1006: the Agent's one terminal edge of a started compaction (``_emit_compaction_done``); an aborted attempt
|
|
1274
|
+
# sends none and ends with the turn.
|
|
1275
|
+
if kind == "compacted":
|
|
1276
|
+
return ["compression_finished"]
|
|
1277
|
+
if kind == "lifecycle":
|
|
883
1278
|
if _is_compression_start(lower):
|
|
884
1279
|
return events + ["compressing"]
|
|
885
1280
|
# The Agent's own compression retries ("Compressed N → M messages, retrying...") are not provider fallbacks.
|
|
886
1281
|
if "compress" in lower or "context reduced to" in lower:
|
|
887
1282
|
return events
|
|
1283
|
+
# Buffered diagnostics change wording across Agent releases; their severity glyph stays stable.
|
|
1284
|
+
if not events and text.startswith(("⚠", "❌")):
|
|
1285
|
+
return ["warning"]
|
|
888
1286
|
if "fallback" in lower or "rate limit" in lower or ("retry" in lower and "non-retryable" not in lower):
|
|
889
1287
|
events.append("warning")
|
|
890
1288
|
return events
|
|
891
1289
|
|
|
892
1290
|
|
|
1291
|
+
#: TAL-821: the Agent's live status line during a rate-limit backoff (``turn_recovery.compute_error_backoff``), e.g.
|
|
1292
|
+
#: "⏳ the free model is busy — resets in ~17m, retrying in 600s (attempt 1/3)". The Agent's other waits are not known
|
|
1293
|
+
#: rate limits: "waiting on provider" covers a 5xx retry as well as a 429 without a reset, and "provider overloaded" is a
|
|
1294
|
+
#: Z.AI overload. ``webui`` is not an attended source, so the Agent sits out a free-tier cooldown instead of ending.
|
|
1295
|
+
_RATE_LIMIT_WAIT_RE = re.compile(r"^⏳ (?:rate limited|the free model is busy) —.* retrying in (\d+)s\b", re.IGNORECASE)
|
|
1296
|
+
#: The line's reset window, in the Agent's ``format_duration_compact`` units ("45s", "17m", "1h 5m", "1.5d").
|
|
1297
|
+
_RESET_IN_RE = re.compile(r"resets in ~((?:\d+(?:\.\d+)?[dhms] ?)+)")
|
|
1298
|
+
_DURATION_UNITS = {"d": 86400, "h": 3600, "m": 60, "s": 1}
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def _rate_limit_wait(text: str) -> tuple[float, str] | None:
|
|
1302
|
+
"""``(seconds, "reset")`` until the provider's limit resets, else ``(seconds, "retry")`` until the Agent retries, for a
|
|
1303
|
+
rate-limit wait line; None for any other line. The reset is the provider's own ``reset_at`` as the Agent read it from
|
|
1304
|
+
the error; the credential pool's exhausted mark is no better, since without that field it is a fixed one-hour guess."""
|
|
1305
|
+
wait = _RATE_LIMIT_WAIT_RE.match(text)
|
|
1306
|
+
if not wait:
|
|
1307
|
+
return None
|
|
1308
|
+
reset = _RESET_IN_RE.search(text)
|
|
1309
|
+
if not reset:
|
|
1310
|
+
return float(wait.group(1)), "retry"
|
|
1311
|
+
return sum(float(amount) * _DURATION_UNITS[unit] for amount, unit in re.findall(r"(\d+(?:\.\d+)?)([dhms])", reset.group(1))), "reset"
|
|
1312
|
+
|
|
1313
|
+
|
|
893
1314
|
def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn, one function
|
|
894
1315
|
session_id = str(params.get("session_id") or "").strip()
|
|
895
1316
|
stream_id = str(params.get("stream_id") or "").strip()
|
|
@@ -909,6 +1330,7 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
909
1330
|
system_message = params.get("system_message")
|
|
910
1331
|
home = profile_home_param(params)
|
|
911
1332
|
run = _Run(stream_id, session_id, ctx)
|
|
1333
|
+
run.home = home
|
|
912
1334
|
with _RUNS_LOCK:
|
|
913
1335
|
if _DRAINING.is_set():
|
|
914
1336
|
raise RpcError("The sidecar is shutting down.", condition="sidecar_unavailable")
|
|
@@ -916,6 +1338,11 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
916
1338
|
# A previous turn for this session that never returned (a tool ignoring its interrupt) still owns the cached
|
|
917
1339
|
# agent; this turn must not share, rebind, or un-interrupt that live instance.
|
|
918
1340
|
session_busy = prior is not None and not prior.finished.is_set()
|
|
1341
|
+
# TAL-829: a second agent beside a turn nobody stopped would wait on that turn's durable turn lease (up to 30
|
|
1342
|
+
# minutes) and then write the same session. A stopped turn only lingers until the Agent abandons the wedged
|
|
1343
|
+
# tool, which lets its lease go within seconds.
|
|
1344
|
+
if session_busy and not (prior.cancel.is_set() or prior.ctx.cancelled):
|
|
1345
|
+
raise RpcError("This chat is still answering the previous message. Stop it or wait for it to finish.", condition="session_busy")
|
|
919
1346
|
_RUNS[stream_id] = run
|
|
920
1347
|
_RUNS_BY_SESSION[session_id] = stream_id
|
|
921
1348
|
raw_emit = ctx.emit
|
|
@@ -945,6 +1372,8 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
945
1372
|
live_tool_calls: list[dict] = []
|
|
946
1373
|
started_ids: set[str] = set()
|
|
947
1374
|
completed_ids: set[str] = set()
|
|
1375
|
+
edit_snapshots: dict[str, Any] = {}
|
|
1376
|
+
prepared_metadata: dict[str, dict] = {}
|
|
948
1377
|
token_sent = [False]
|
|
949
1378
|
|
|
950
1379
|
def on_token(text):
|
|
@@ -970,6 +1399,9 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
970
1399
|
return
|
|
971
1400
|
if tid:
|
|
972
1401
|
started_ids.add(tid)
|
|
1402
|
+
snapshot = _edit_snapshot(name, args, session_id)
|
|
1403
|
+
if snapshot is not None:
|
|
1404
|
+
edit_snapshots[tid] = snapshot
|
|
973
1405
|
live_tool_calls.append({"name": name, "args": args if isinstance(args, dict) else {}, "tid": tid, "done": False})
|
|
974
1406
|
emit("tool", {"event_type": "tool.started", "name": name, "preview": None, "args": _args_snapshot(args), "tid": tid})
|
|
975
1407
|
|
|
@@ -988,8 +1420,12 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
988
1420
|
call["snippet"] = snippet
|
|
989
1421
|
break
|
|
990
1422
|
payload = {"event_type": "tool.completed", "name": name, "preview": snippet, "args": _args_snapshot(args), "tid": tid, "raw_result": _raw_result(function_result)}
|
|
1423
|
+
# TAL-909: the metadata the Agent persisted on the result row, or, from an Agent without the callback, live only.
|
|
1424
|
+
metadata = prepared_metadata.pop(tid) if tid in prepared_metadata else _edit_metadata(name, args, function_result, edit_snapshots.pop(tid, None))
|
|
1425
|
+
if metadata:
|
|
1426
|
+
payload["display_metadata"] = metadata
|
|
991
1427
|
# TAL-397: ``raw_result`` caps nested values, so the todo tool's list travels whole for the server's ``todo_state``.
|
|
992
|
-
if
|
|
1428
|
+
if function_result is not None and _is_todo_tool(name):
|
|
993
1429
|
payload["todo_result"] = function_result if isinstance(function_result, str) else json.dumps(function_result, default=str)
|
|
994
1430
|
# TAL-448: likewise a result's unified ``diff``, so the server's ``edit_diff`` counts cover the whole change.
|
|
995
1431
|
diff = _result_diff(function_result)
|
|
@@ -1004,6 +1440,14 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1004
1440
|
break
|
|
1005
1441
|
emit("tool_complete", payload)
|
|
1006
1442
|
|
|
1443
|
+
def on_tool_result_metadata(tool_call_id, name, args, function_result):
|
|
1444
|
+
"""TAL-909: called before the Agent appends and flushes the tool-result row, which keeps the returned dict as
|
|
1445
|
+
its ``display_metadata``; the completion frame that follows carries the same dict."""
|
|
1446
|
+
tid = str(tool_call_id or "")
|
|
1447
|
+
metadata = _edit_metadata(name, args, function_result, edit_snapshots.pop(tid, None))
|
|
1448
|
+
prepared_metadata[tid] = metadata
|
|
1449
|
+
return metadata
|
|
1450
|
+
|
|
1007
1451
|
def on_tool(*cb_args, **cb_kwargs):
|
|
1008
1452
|
# Structured callbacks carry tool cards; the progress callback only feeds reasoning text on older builds.
|
|
1009
1453
|
if len(cb_args) >= 3 and cb_args[0] in ("reasoning.available", "_thinking"):
|
|
@@ -1016,43 +1460,172 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1016
1460
|
for event in _status_events(kind, text):
|
|
1017
1461
|
if event == "compressing":
|
|
1018
1462
|
emit("compressing", {"session_id": session_id, "message": "Compressing context"})
|
|
1463
|
+
elif event == "compression_finished":
|
|
1464
|
+
emit("compression_finished", {"session_id": session_id})
|
|
1019
1465
|
elif event == "warning":
|
|
1020
1466
|
emit("warning", {"type": "fallback", "message": text})
|
|
1021
1467
|
else:
|
|
1022
1468
|
emit("status", {"kind": "terminal_error", "message": text})
|
|
1023
1469
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1470
|
+
wait_state = {"active": False, "prompt": False}
|
|
1471
|
+
|
|
1472
|
+
def on_thinking(text):
|
|
1473
|
+
"""Normalize provider waits and prompt-reading progress from the Agent's live status callback."""
|
|
1474
|
+
line = str(text or "").strip()
|
|
1475
|
+
progress = re.fullmatch(r"⚙ processing prompt(?: — (\d+(?:\.\d+)?)%)?", line)
|
|
1476
|
+
if progress:
|
|
1477
|
+
wait_state["prompt"] = True
|
|
1478
|
+
emit("provider_phase", {"phase": {"phase": "prompt_reading", "progress_percent": min(100, float(progress[1])) if progress[1] else None}})
|
|
1479
|
+
elif wait_state["prompt"]:
|
|
1480
|
+
wait_state["prompt"] = False
|
|
1481
|
+
emit("provider_phase", {"phase": None})
|
|
1482
|
+
wait = _rate_limit_wait(line)
|
|
1483
|
+
if wait is not None:
|
|
1484
|
+
wait_state["active"] = True
|
|
1485
|
+
emit("rate_limit_wait", {"provider": str(getattr(run.agent, "provider", None) or resolved_provider or "") or None,
|
|
1486
|
+
"until": time.time() + wait[0], "until_kind": wait[1]})
|
|
1487
|
+
elif wait_state["active"]:
|
|
1488
|
+
wait_state["active"] = False
|
|
1489
|
+
emit("rate_limit_wait", {"provider": None, "until": None})
|
|
1490
|
+
|
|
1491
|
+
# TAL-803: the Agent's sticky ``AgentNotice``s (a startup warning such as a memory provider that failed to load,
|
|
1492
|
+
# credit and usage bands) and their clears; the server keeps the session's active set.
|
|
1493
|
+
def on_notice(notice):
|
|
1494
|
+
text = str(getattr(notice, "text", "") or "").strip()
|
|
1495
|
+
if not text:
|
|
1496
|
+
return
|
|
1497
|
+
ttl_ms = getattr(notice, "ttl_ms", None)
|
|
1498
|
+
key = getattr(notice, "key", None)
|
|
1499
|
+
# The Agent raises a startup warning (memory provider unavailable) once per process, so a rebuilt agent never
|
|
1500
|
+
# repeats it: it outlives the agent that raised it. Credit notices are per agent and re-raised by the next one.
|
|
1501
|
+
emit("notice", {"id": getattr(notice, "id", None), "key": key, "level": str(getattr(notice, "level", "") or "info"),
|
|
1502
|
+
"kind": str(getattr(notice, "kind", "") or "sticky"), "ttl_ms": ttl_ms if isinstance(ttl_ms, int) else None, "text": text,
|
|
1503
|
+
"lapses_with_agent": not str(key or "").startswith("startup-warning.")})
|
|
1504
|
+
|
|
1505
|
+
def on_notice_clear(key):
|
|
1506
|
+
if key:
|
|
1507
|
+
emit("notice_clear", {"key": str(key)})
|
|
1508
|
+
|
|
1509
|
+
def ask(data: dict, timeout: int) -> str | None:
|
|
1510
|
+
"""Park on a prompt the server relays until it is answered, the turn is cancelled or ends, or ``timeout``
|
|
1511
|
+
passes (``<= 0``: no limit). The answer, or None, goes back to the Agent and is never emitted."""
|
|
1036
1512
|
entry = _ClarifyEntry(data)
|
|
1037
1513
|
entry.data["clarify_id"] = entry.clarify_id
|
|
1514
|
+
entry.deadline = None if timeout <= 0 else time.monotonic() + float(timeout)
|
|
1038
1515
|
with run.lock:
|
|
1039
1516
|
run.clarify_entries[entry.clarify_id] = entry
|
|
1040
1517
|
emit("clarify", dict(entry.data))
|
|
1041
|
-
deadline = None if timeout <= 0 else time.monotonic() + float(timeout)
|
|
1042
1518
|
while not entry.event.is_set():
|
|
1043
|
-
if run.cancel.is_set() or run.finished.is_set():
|
|
1519
|
+
if run.cancel.is_set() or run.finished.is_set() or (data.get("kind") == "skill_secret" and (ctx.cancelled or run.session_ended)):
|
|
1044
1520
|
break
|
|
1045
|
-
if deadline is None:
|
|
1521
|
+
if entry.deadline is None:
|
|
1046
1522
|
entry.event.wait(1.0)
|
|
1047
1523
|
continue
|
|
1048
|
-
remaining = deadline - time.monotonic()
|
|
1524
|
+
remaining = entry.deadline - time.monotonic()
|
|
1049
1525
|
if remaining <= 0:
|
|
1050
1526
|
break
|
|
1051
1527
|
entry.event.wait(min(1.0, remaining))
|
|
1052
1528
|
with run.lock:
|
|
1053
1529
|
run.clarify_entries.pop(entry.clarify_id, None)
|
|
1054
1530
|
emit("clarify_resolved", {"clarify_id": entry.clarify_id, "session_id": session_id})
|
|
1055
|
-
return entry.result
|
|
1531
|
+
return entry.result
|
|
1532
|
+
|
|
1533
|
+
def ask_vault(kind: str, fields: dict) -> str:
|
|
1534
|
+
"""TAL-887: a password-manager prompt; an empty answer is the Agent's decline, as are expiry and cancel."""
|
|
1535
|
+
timeout = _VAULT_PROMPT_TIMEOUTS[kind]
|
|
1536
|
+
data = {"kind": kind, "session_id": session_id, "requested_at": time.time(), "timeout_seconds": timeout, **fields}
|
|
1537
|
+
return ask(data, timeout) or ""
|
|
1538
|
+
|
|
1539
|
+
def vault_unlock_prompt(backend, display_name):
|
|
1540
|
+
# The Agent stores the unlock right after this returns, on the thread that called it: a tool worker
|
|
1541
|
+
# inherits this callback but not the turn's session, so bind it here too, or closing the session could
|
|
1542
|
+
# not lock the manager again.
|
|
1543
|
+
_set_vault_session(session_id)
|
|
1544
|
+
return ask_vault("vault_unlock", {"backend": str(backend), "display_name": str(display_name)})
|
|
1545
|
+
|
|
1546
|
+
def vault_save_login_prompt(origin, site):
|
|
1547
|
+
# The server sends ``{"identifier", "password"}``; the Agent stores it in its vault and fills the page.
|
|
1548
|
+
raw = ask_vault("vault_save_login", {"origin": str(origin), "site": str(site)})
|
|
1549
|
+
try:
|
|
1550
|
+
answer = json.loads(raw) if raw else None
|
|
1551
|
+
except ValueError:
|
|
1552
|
+
return None
|
|
1553
|
+
return answer if isinstance(answer, dict) and answer.get("password") else None
|
|
1554
|
+
|
|
1555
|
+
def vault_code_prompt(site, hint):
|
|
1556
|
+
return ask_vault("vault_code", {"site": str(site), "hint": str(hint or "")})
|
|
1557
|
+
|
|
1558
|
+
def skill_secret_prompt(env_var, prompt, metadata=None):
|
|
1559
|
+
skipped = {"success": True, "stored_as": env_var, "validated": False, "skipped": True}
|
|
1560
|
+
if not isinstance(env_var, str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", env_var):
|
|
1561
|
+
return skipped
|
|
1562
|
+
metadata = metadata if isinstance(metadata, dict) else {}
|
|
1563
|
+
fields = {"env_var": env_var, "prompt": str(prompt), **{key: str(metadata.get(key) or "") for key in ("skill_name", "help", "required_for")}}
|
|
1564
|
+
value = ask_vault("skill_secret", fields)
|
|
1565
|
+
if not value.strip():
|
|
1566
|
+
return skipped
|
|
1567
|
+
# The native writer strips these bytes, and reports non-ASCII characters to stderr. Refuse rather
|
|
1568
|
+
# than change the credential or expose any of its characters in logs.
|
|
1569
|
+
if not value.isascii() or any(char in value for char in ("\r", "\n", "\0")):
|
|
1570
|
+
return {**skipped, "success": False}
|
|
1571
|
+
try:
|
|
1572
|
+
from hermes_cli.config import save_env_value_secure
|
|
1573
|
+
from hermes_constants import get_hermes_home
|
|
1574
|
+
|
|
1575
|
+
# Keep the inherited secret scope so setup can use the value immediately. Fence the write against
|
|
1576
|
+
# eviction/replacement under the same lock that changes the run's identity and session lifetime.
|
|
1577
|
+
with _RUNS_LOCK:
|
|
1578
|
+
if (ctx.cancelled or run.cancel.is_set() or run.finished.is_set() or run.session_ended
|
|
1579
|
+
or _RUNS_BY_SESSION.get(session_id) != stream_id or _RUNS.get(stream_id) is not run
|
|
1580
|
+
or get_hermes_home() != home):
|
|
1581
|
+
return skipped
|
|
1582
|
+
saved = save_env_value_secure(env_var, value)
|
|
1583
|
+
return {"success": saved.get("success") is True, "stored_as": env_var, "validated": saved.get("validated") is True, "skipped": False}
|
|
1584
|
+
except Exception: # noqa: BLE001 - never expose an exception that could include the submitted value
|
|
1585
|
+
return {**skipped, "success": False}
|
|
1586
|
+
|
|
1587
|
+
def clarify_callback(question, choices=None, multi_select=False, questions=None):
|
|
1588
|
+
# TAL-351: remove the legacy signature path when v0.21.6 is the oldest supported Agent.
|
|
1589
|
+
structured = isinstance(question, list)
|
|
1590
|
+
if structured:
|
|
1591
|
+
questions, question = question, ""
|
|
1592
|
+
choices_list = [str(c) for c in (choices or [])]
|
|
1593
|
+
# Predecessor `_clarify_timeout_seconds`: the Agent's own resolver over this profile's config
|
|
1594
|
+
# (``clarify.timeout`` else ``agent.clarify_timeout`` else 3600); ``<= 0`` waits until answered or cancelled.
|
|
1595
|
+
# The advertised ``timeout_seconds`` is what the clients count down, so it is the same number.
|
|
1596
|
+
timeout = _clarify_timeout(params)
|
|
1597
|
+
data = {"question": str(question or ""), "choices_offered": choices_list, "session_id": session_id, "kind": "clarify", "requested_at": time.time(), "timeout_seconds": timeout}
|
|
1598
|
+
if multi_select:
|
|
1599
|
+
data["multi_select"] = True
|
|
1600
|
+
# Batch prompts (the Agent's ``_run_batch``) carry no top-level question and expect the ``{"answers": {...}}`` reply.
|
|
1601
|
+
if isinstance(questions, list) and questions:
|
|
1602
|
+
data["questions"] = questions
|
|
1603
|
+
answer = ask(data, timeout)
|
|
1604
|
+
if answer is not None:
|
|
1605
|
+
if structured:
|
|
1606
|
+
return {"answers": json.loads(answer)["answers"], "outcome": "submitted"}
|
|
1607
|
+
return answer
|
|
1608
|
+
cancelled = run.cancel.is_set() or run.finished.is_set()
|
|
1609
|
+
if structured:
|
|
1610
|
+
return {"answers": {}, "outcome": "cancelled" if cancelled else "timed_out",
|
|
1611
|
+
"notice": "The user cancelled the clarification." if cancelled else "The clarification timed out."}
|
|
1612
|
+
# Older tools have no outcome field. A batch understands timed_out; a single needs an empty answer.
|
|
1613
|
+
if questions:
|
|
1614
|
+
return json.dumps({"answers": {}, "timed_out": not cancelled})
|
|
1615
|
+
return ""
|
|
1616
|
+
|
|
1617
|
+
# TAL-818: a ``manage_connections`` card (a hosted connect, an MCP install) opened; the server turns the frame into
|
|
1618
|
+
# the chat's connect card, which reads and answers the operation through ``connectors.operation`` / ``respond``.
|
|
1619
|
+
# Only a client that draws the card asks for it: without the callback the Agent answers with plain links at once.
|
|
1620
|
+
connect_card = params.get("connect_card") is True
|
|
1621
|
+
|
|
1622
|
+
def on_connection(payload):
|
|
1623
|
+
from .connectors import remember_chat_operation
|
|
1624
|
+
|
|
1625
|
+
view = remember_chat_operation(home, payload)
|
|
1626
|
+
if view is not None:
|
|
1627
|
+
emit("connection", view)
|
|
1628
|
+
return None
|
|
1056
1629
|
|
|
1057
1630
|
# Approval bridge: the Agent parks dangerous commands in its gateway queue and calls back here.
|
|
1058
1631
|
try:
|
|
@@ -1061,51 +1634,21 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1061
1634
|
register_gateway_notify = unregister_gateway_notify = None
|
|
1062
1635
|
approval_cb = _approval_notifier(session_id, emit) if register_gateway_notify is not None else None
|
|
1063
1636
|
|
|
1064
|
-
kwargs
|
|
1065
|
-
|
|
1066
|
-
provider=resolved_provider,
|
|
1067
|
-
base_url=runtime.get("base_url"),
|
|
1068
|
-
api_key=runtime.get("api_key"),
|
|
1069
|
-
platform="webui",
|
|
1070
|
-
quiet_mode=True,
|
|
1071
|
-
enabled_toolsets=toolsets,
|
|
1072
|
-
session_id=session_id,
|
|
1073
|
-
stream_delta_callback=on_token,
|
|
1074
|
-
reasoning_callback=on_reasoning,
|
|
1075
|
-
tool_progress_callback=on_tool,
|
|
1076
|
-
clarify_callback=clarify_callback,
|
|
1077
|
-
)
|
|
1637
|
+
kwargs = _agent_kwargs(AIAgent, params, session_id, resolved_model, resolved_provider, runtime, toolsets)
|
|
1638
|
+
kwargs.update(stream_delta_callback=on_token, reasoning_callback=on_reasoning, tool_progress_callback=on_tool, clarify_callback=clarify_callback)
|
|
1078
1639
|
for name, value in (
|
|
1079
1640
|
("interim_assistant_callback", on_interim),
|
|
1080
1641
|
("tool_start_callback", on_tool_start),
|
|
1081
1642
|
("tool_complete_callback", on_tool_complete),
|
|
1643
|
+
("tool_result_metadata_callback", on_tool_result_metadata),
|
|
1082
1644
|
("status_callback", on_status),
|
|
1083
|
-
("
|
|
1084
|
-
("
|
|
1085
|
-
("
|
|
1086
|
-
("
|
|
1087
|
-
("gateway_session_key", session_id),
|
|
1645
|
+
("thinking_callback", on_thinking),
|
|
1646
|
+
("notice_callback", on_notice),
|
|
1647
|
+
("notice_clear_callback", on_notice_clear),
|
|
1648
|
+
("connection_callback", on_connection if connect_card else None),
|
|
1088
1649
|
):
|
|
1089
1650
|
if _supported(AIAgent, name) and value is not None:
|
|
1090
1651
|
kwargs[name] = value
|
|
1091
|
-
if isinstance(params.get("max_iterations"), int) and params["max_iterations"] > 0 and _supported(AIAgent, "max_iterations"):
|
|
1092
|
-
kwargs["max_iterations"] = params["max_iterations"]
|
|
1093
|
-
if isinstance(params.get("max_tokens"), int) and params["max_tokens"] > 0 and _supported(AIAgent, "max_tokens"):
|
|
1094
|
-
kwargs["max_tokens"] = params["max_tokens"]
|
|
1095
|
-
# Predecessor: `agent.reasoning_effort` (already coerced for the model) → AIAgent ``reasoning_config``.
|
|
1096
|
-
reasoning_config = params.get("reasoning_config")
|
|
1097
|
-
if isinstance(reasoning_config, dict) and _supported(AIAgent, "reasoning_config"):
|
|
1098
|
-
kwargs["reasoning_config"] = reasoning_config
|
|
1099
|
-
fallback_chain = _profile_fallback_chain()
|
|
1100
|
-
if fallback_chain and _supported(AIAgent, "fallback_model"):
|
|
1101
|
-
kwargs["fallback_model"] = fallback_chain
|
|
1102
|
-
request_overrides = _main_model_request_overrides(resolved_model, resolved_provider)
|
|
1103
|
-
if request_overrides and _supported(AIAgent, "request_overrides"):
|
|
1104
|
-
kwargs["request_overrides"] = request_overrides
|
|
1105
|
-
# TAL-913: the profile's ``checkpoints`` section, as the Agent's own CLI and gateway turns read it.
|
|
1106
|
-
for name, value in checkpoint_kwargs().items():
|
|
1107
|
-
if _supported(AIAgent, name):
|
|
1108
|
-
kwargs[name] = value
|
|
1109
1652
|
signature = _agent_signature(resolved_model, resolved_provider, runtime, toolsets, str(params.get("profile_home")), kwargs)
|
|
1110
1653
|
agent = None
|
|
1111
1654
|
|
|
@@ -1113,6 +1656,8 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1113
1656
|
"""Claim the agent for this run under ``_AGENT_CACHE_LOCK``, so an eviction never releases it mid-turn."""
|
|
1114
1657
|
run.prior_messages = getattr(bound, "_session_messages", None)
|
|
1115
1658
|
run.agent = bound
|
|
1659
|
+
if run.session_ended:
|
|
1660
|
+
bound.skip_background_review = True
|
|
1116
1661
|
|
|
1117
1662
|
if not session_busy:
|
|
1118
1663
|
with _AGENT_CACHE_LOCK:
|
|
@@ -1122,27 +1667,31 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1122
1667
|
_AGENT_CACHE.move_to_end(session_id)
|
|
1123
1668
|
bind(agent)
|
|
1124
1669
|
if agent is not None:
|
|
1125
|
-
for name in ("stream_delta_callback", "reasoning_callback", "tool_progress_callback", "clarify_callback", "interim_assistant_callback", "tool_start_callback", "tool_complete_callback", "status_callback"):
|
|
1670
|
+
for name in ("stream_delta_callback", "reasoning_callback", "tool_progress_callback", "clarify_callback", "interim_assistant_callback", "tool_start_callback", "tool_complete_callback", "tool_result_metadata_callback", "status_callback", "thinking_callback", "notice_callback", "notice_clear_callback"):
|
|
1126
1671
|
if name in kwargs and hasattr(agent, name):
|
|
1127
1672
|
setattr(agent, name, kwargs[name])
|
|
1673
|
+
# A cached agent keeps the last turn's card callback; this turn's client decides afresh (TAL-818).
|
|
1674
|
+
if hasattr(agent, "connection_callback"):
|
|
1675
|
+
agent.connection_callback = on_connection if connect_card else None
|
|
1676
|
+
# TAL-902: an agent ``/refine`` built runs its first turn here. Like a new one, it lapses the notices of the
|
|
1677
|
+
# agent it replaced and raises the startup notices it parked while it had no callback.
|
|
1678
|
+
if vars(agent).pop("_talaria_unannounced", False):
|
|
1679
|
+
emit("notices_reset", {})
|
|
1680
|
+
replay = getattr(agent, "_replay_startup_warnings", None)
|
|
1681
|
+
if callable(replay):
|
|
1682
|
+
replay()
|
|
1128
1683
|
else:
|
|
1684
|
+
# TAL-803: the notices a replaced agent raised lapse with it (``lapses_with_agent``); this one raises its own.
|
|
1685
|
+
emit("notices_reset", {})
|
|
1129
1686
|
# A ``/btw`` side question's throwaway session stays out of state.db.
|
|
1130
|
-
|
|
1131
|
-
if session_db is not None:
|
|
1132
|
-
kwargs["session_db"] = session_db
|
|
1133
|
-
kwargs["session_id"] = _execution_session_id(session_db, session_id)
|
|
1134
|
-
try:
|
|
1135
|
-
agent = AIAgent(**kwargs)
|
|
1136
|
-
except BaseException:
|
|
1137
|
-
_release_session_db(session_db)
|
|
1138
|
-
raise
|
|
1139
|
-
agent._talaria_session_db = session_db # released with the agent (``_release_agent``)
|
|
1687
|
+
agent = _build_agent(AIAgent, kwargs, home, session_id, state_db=not params.get("ephemeral"))
|
|
1140
1688
|
with _AGENT_CACHE_LOCK:
|
|
1141
1689
|
bind(agent)
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1690
|
+
if not run.session_ended:
|
|
1691
|
+
replaced = _AGENT_CACHE.pop(session_id, None)
|
|
1692
|
+
_AGENT_CACHE[session_id] = (agent, signature, home)
|
|
1693
|
+
_release_agents_locked([replaced] if replaced else [])
|
|
1694
|
+
_evict_idle_agents_locked()
|
|
1146
1695
|
# Predecessor ``agent.ephemeral_system_prompt``: personality, surface context, progress guidance, and delivery
|
|
1147
1696
|
# hints travel as runtime instructions that are never persisted to history.
|
|
1148
1697
|
ephemeral = params.get("ephemeral_system_prompt")
|
|
@@ -1189,27 +1738,40 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1189
1738
|
watcher = threading.Thread(target=_watch_cancel, daemon=True, name=f"chat-cancel-{stream_id[:8]}")
|
|
1190
1739
|
watcher.start()
|
|
1191
1740
|
run_kwargs: dict = {"user_message": user_message, "conversation_history": history, "task_id": session_id}
|
|
1192
|
-
|
|
1193
|
-
run_params = set(inspect.signature(agent.run_conversation).parameters)
|
|
1194
|
-
except (TypeError, ValueError):
|
|
1195
|
-
run_params = set()
|
|
1741
|
+
run_params = _run_conversation_parameters(type(agent))
|
|
1196
1742
|
if system_message and ("system_message" in run_params or not run_params):
|
|
1197
1743
|
run_kwargs["system_message"] = system_message
|
|
1744
|
+
# Talaria generates and syncs the title; suppress the Agent's duplicate auxiliary request.
|
|
1745
|
+
if "title_user_message" in run_params:
|
|
1746
|
+
run_kwargs["title_user_message"] = ""
|
|
1198
1747
|
# The server's clean user text (no workspace prefix) is what state.db stores; the Agent keeps the sent bytes beside it.
|
|
1199
1748
|
persist = params.get("persist_user_message")
|
|
1200
1749
|
if not (isinstance(persist, str) and persist.strip()):
|
|
1201
1750
|
persist = user_message if isinstance(user_message, str) else None
|
|
1202
1751
|
if "persist_user_message" in run_params and persist is not None:
|
|
1203
1752
|
run_kwargs["persist_user_message"] = persist
|
|
1753
|
+
# TAL-801: a spoken turn runs on the profile's ``auxiliary.voice_chat`` slot; the Agent consumes the mark at turn
|
|
1754
|
+
# setup. Every turn sets it, as the Agent's own surfaces do, so a mark a failed turn never consumed cannot leak.
|
|
1755
|
+
if _has_voice_turn_route():
|
|
1756
|
+
agent._voice_turn_pending = params.get("voice_turn") is True
|
|
1204
1757
|
error: str | None = None
|
|
1205
1758
|
result: dict = {}
|
|
1759
|
+
# The Agent reads its vault prompts from the thread running the turn (and copies them onto tool workers).
|
|
1760
|
+
_set_vault_prompts(session_id, vault_unlock_prompt, vault_save_login_prompt, vault_code_prompt)
|
|
1206
1761
|
try:
|
|
1207
|
-
|
|
1762
|
+
try:
|
|
1763
|
+
from tools.skills_tool import bind_turn_secret_capture
|
|
1764
|
+
except ImportError: # Older Agents and v0.22.2 have only an unsafe process-global callback slot.
|
|
1765
|
+
bind_turn_secret_capture = None
|
|
1766
|
+
log.debug("Agent lacks turn-local skill secret capture; secure setup is unavailable")
|
|
1767
|
+
with bind_turn_secret_capture(skill_secret_prompt) if bind_turn_secret_capture and params.get("skill_secret") is True else contextlib.nullcontext():
|
|
1768
|
+
raw = agent.run_conversation(**run_kwargs)
|
|
1208
1769
|
result = raw if isinstance(raw, dict) else {}
|
|
1209
1770
|
except Exception as exc: # noqa: BLE001
|
|
1210
1771
|
log.warning("agent turn failed for %s: %s", session_id, exc)
|
|
1211
1772
|
error = f"{type(exc).__name__}: {exc}"
|
|
1212
1773
|
finally:
|
|
1774
|
+
_set_vault_prompts(None, None, None, None)
|
|
1213
1775
|
run.finished.set()
|
|
1214
1776
|
watcher.join(timeout=2)
|
|
1215
1777
|
# A successor turn for the same session (admitted past the cancel-unwind ceiling) may have registered
|
|
@@ -1255,7 +1817,8 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1255
1817
|
"max_iterations_summary_request": _max_iterations_summary_request(),
|
|
1256
1818
|
"usage": _usage(agent),
|
|
1257
1819
|
"context": _context_length(agent),
|
|
1258
|
-
|
|
1820
|
+
# TAL-801: the Agent's result names the model the turn ran on; a voice turn's route is undone by now.
|
|
1821
|
+
"model": str(result.get("model") or getattr(agent, "model", None) or resolved_model),
|
|
1259
1822
|
"provider": str(resolved_provider or ""),
|
|
1260
1823
|
"compressed": compressed,
|
|
1261
1824
|
"agent_session_id": str(getattr(agent, "session_id", None) or session_id),
|
|
@@ -1269,6 +1832,10 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1269
1832
|
_RUNS.pop(stream_id, None)
|
|
1270
1833
|
if _RUNS_BY_SESSION.get(session_id) == stream_id:
|
|
1271
1834
|
_RUNS_BY_SESSION.pop(session_id, None)
|
|
1835
|
+
# TAL-887: a session ended mid-turn (``chat.evict_agent`` marks the run under these locks) released its
|
|
1836
|
+
# unlocks then; one the Agent stored after that is released now.
|
|
1837
|
+
if run.session_ended:
|
|
1838
|
+
_release_vault_session(session_id)
|
|
1272
1839
|
# An agent evicted or replaced while this turn held it is released now that the turn is over.
|
|
1273
1840
|
_release_if_dropped_locked(run.agent, home)
|
|
1274
1841
|
|
|
@@ -1351,6 +1918,7 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1351
1918
|
platform="webui",
|
|
1352
1919
|
quiet_mode=True,
|
|
1353
1920
|
enabled_toolsets=toolsets,
|
|
1921
|
+
disabled_toolsets=disabled_toolsets(),
|
|
1354
1922
|
session_id=session_id,
|
|
1355
1923
|
)
|
|
1356
1924
|
for name, value in (
|
|
@@ -1360,6 +1928,8 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1360
1928
|
("credential_pool", runtime.get("credential_pool")),
|
|
1361
1929
|
("gateway_session_key", session_id),
|
|
1362
1930
|
("skip_memory", not _checkpoint_required()),
|
|
1931
|
+
("request_overrides", _request_overrides(kwargs["model"], kwargs["provider"], runtime)),
|
|
1932
|
+
("prefill_messages", _profile_prefill_messages()),
|
|
1363
1933
|
):
|
|
1364
1934
|
if _supported(AIAgent, name) and value is not None:
|
|
1365
1935
|
kwargs[name] = value
|
|
@@ -1398,6 +1968,93 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1398
1968
|
_release_compression(agent, committed=False)
|
|
1399
1969
|
|
|
1400
1970
|
|
|
1971
|
+
_REFINE_BUSY = "Agent is running — wait for the turn to finish, then /refine."
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
def _session_running_locked(session_id: str) -> bool:
|
|
1975
|
+
"""Whether a turn for ``session_id`` has not returned yet, or no turn can start (caller holds ``_RUNS_LOCK``)."""
|
|
1976
|
+
run = _RUNS.get(_RUNS_BY_SESSION.get(session_id) or "")
|
|
1977
|
+
return _DRAINING.is_set() or (run is not None and not run.finished.is_set())
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def _refine_agent(params: dict, session_id: str, home):
|
|
1981
|
+
"""The session's cached agent, else one built the way its turn builds it and cached for that turn, held in
|
|
1982
|
+
``_COMMITTING`` until the caller lets go; None when a turn started meanwhile. Runs under the call's ``scoped_home``."""
|
|
1983
|
+
with _AGENT_CACHE_LOCK:
|
|
1984
|
+
with _RUNS_LOCK:
|
|
1985
|
+
# Checked again with the claim: a turn admitted since ``refine``'s check holds the cached agent.
|
|
1986
|
+
if _session_running_locked(session_id):
|
|
1987
|
+
return None
|
|
1988
|
+
cached = _AGENT_CACHE.get(session_id)
|
|
1989
|
+
if cached is not None:
|
|
1990
|
+
_COMMITTING.append(cached[0])
|
|
1991
|
+
return cached[0]
|
|
1992
|
+
model = str(params.get("model") or "").strip()
|
|
1993
|
+
provider = str(params.get("model_provider") or "").strip() or None
|
|
1994
|
+
toolsets = _turn_toolsets(params.get("enabled_toolsets"))
|
|
1995
|
+
# Like a turn: the profile's MCP servers connect before the agent snapshots its tools, which its next turn keeps.
|
|
1996
|
+
try:
|
|
1997
|
+
from .commands import discover_profile_mcp_tools
|
|
1998
|
+
|
|
1999
|
+
discover_profile_mcp_tools()
|
|
2000
|
+
except Exception: # noqa: BLE001
|
|
2001
|
+
log.warning("MCP discovery failed", exc_info=True)
|
|
2002
|
+
runtime = _resolve_runtime(provider, model)
|
|
2003
|
+
resolved_model = model or str(runtime.get("model") or "")
|
|
2004
|
+
resolved_provider = provider or runtime.get("provider")
|
|
2005
|
+
AIAgent = _agent_class()
|
|
2006
|
+
kwargs = _agent_kwargs(AIAgent, params, session_id, resolved_model, resolved_provider, runtime, toolsets)
|
|
2007
|
+
signature = _agent_signature(resolved_model, resolved_provider, runtime, toolsets, str(params.get("profile_home")), kwargs)
|
|
2008
|
+
built = _build_agent(AIAgent, kwargs, home, session_id)
|
|
2009
|
+
with _AGENT_CACHE_LOCK:
|
|
2010
|
+
with _RUNS_LOCK:
|
|
2011
|
+
running = _session_running_locked(session_id)
|
|
2012
|
+
cached = _AGENT_CACHE.get(session_id)
|
|
2013
|
+
agent = None if running else cached[0] if cached else built
|
|
2014
|
+
if agent is not None:
|
|
2015
|
+
_COMMITTING.append(agent)
|
|
2016
|
+
if agent is built:
|
|
2017
|
+
built._talaria_unannounced = True # its first turn announces it (``start``)
|
|
2018
|
+
_AGENT_CACHE[session_id] = (built, signature, home)
|
|
2019
|
+
_evict_idle_agents_locked()
|
|
2020
|
+
else:
|
|
2021
|
+
_release_agents_locked([(built, None, home)])
|
|
2022
|
+
return agent
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def refine(params: dict) -> str:
|
|
2026
|
+
"""TAL-902: ``/refine [focus]`` like the Agent's live-session handler (``_format_live_refine_output``): the memory/skill
|
|
2027
|
+
review fork of ``messages`` (the server's model context for the session) on the session's agent, never while its turn
|
|
2028
|
+
runs. The fork carries this call's ``scoped_home``, so its memory and skill writes land in the session's profile."""
|
|
2029
|
+
session_id = str(params.get("session_id") or "").strip()
|
|
2030
|
+
messages = params.get("messages")
|
|
2031
|
+
if not session_id or not isinstance(messages, list):
|
|
2032
|
+
raise InvalidParams("session_id and messages are required")
|
|
2033
|
+
focus = str(params.get("focus") or "").strip()
|
|
2034
|
+
with _RUNS_LOCK:
|
|
2035
|
+
if _session_running_locked(session_id):
|
|
2036
|
+
return _REFINE_BUSY
|
|
2037
|
+
if not messages:
|
|
2038
|
+
return "Nothing to refine yet — the conversation is empty."
|
|
2039
|
+
home = profile_home_param(params)
|
|
2040
|
+
agent = _refine_agent(params, session_id, home)
|
|
2041
|
+
if agent is None:
|
|
2042
|
+
return _REFINE_BUSY
|
|
2043
|
+
try:
|
|
2044
|
+
agent._spawn_background_review(messages_snapshot=list(messages), review_memory=True,
|
|
2045
|
+
review_skills="skill_manage" in (getattr(agent, "valid_tool_names", None) or set()),
|
|
2046
|
+
focus=focus or None, explicit=True)
|
|
2047
|
+
except Exception as exc: # noqa: BLE001 - the Agent's own wording for a fork that could not start
|
|
2048
|
+
log.warning("/refine failed to start for session %s", session_id, exc_info=True)
|
|
2049
|
+
return f"/refine failed to start: {exc}"
|
|
2050
|
+
finally:
|
|
2051
|
+
with _AGENT_CACHE_LOCK:
|
|
2052
|
+
_COMMITTING.remove(agent)
|
|
2053
|
+
_release_if_dropped_locked(agent, home)
|
|
2054
|
+
tail = f" (focus: {focus})" if focus else ""
|
|
2055
|
+
return f"⚗ Reviewing this conversation in the background{tail} — any memory/skill updates will be reported when done."
|
|
2056
|
+
|
|
2057
|
+
|
|
1401
2058
|
def _run_for(params: dict) -> _Run | None:
|
|
1402
2059
|
stream_id = str(params.get("stream_id") or "").strip()
|
|
1403
2060
|
session_id = str(params.get("session_id") or "").strip()
|
|
@@ -1426,6 +2083,11 @@ def register(registry) -> None:
|
|
|
1426
2083
|
with scoped_home(profile_home_param(params)):
|
|
1427
2084
|
return compress(ctx, params)
|
|
1428
2085
|
|
|
2086
|
+
@registry.method("chat.refine")
|
|
2087
|
+
def refine_(ctx: CallContext, params: dict) -> dict:
|
|
2088
|
+
with scoped_home(profile_home_param(params)):
|
|
2089
|
+
return {"output": refine(params)}
|
|
2090
|
+
|
|
1429
2091
|
@registry.method("chat.compress_finalize", requires_agent=False)
|
|
1430
2092
|
def compress_finalize_(ctx: CallContext, params: dict) -> dict:
|
|
1431
2093
|
token = str(params.get("commit_token") or "").strip()
|
|
@@ -1487,28 +2149,74 @@ def register(registry) -> None:
|
|
|
1487
2149
|
pending, index = _steer_queue_params(params)
|
|
1488
2150
|
run = _run_for(params)
|
|
1489
2151
|
if run is None or run.agent is None:
|
|
1490
|
-
return {"redirected": False, "withdrawn": False}
|
|
2152
|
+
return {"redirected": False, "withdrawn": False, "reason": "not_running"}
|
|
1491
2153
|
return steer_now(run.agent, pending, index)
|
|
1492
2154
|
|
|
2155
|
+
@registry.method("chat.finalize_session", requires_agent=False)
|
|
2156
|
+
def finalize_(ctx: CallContext, params: dict) -> dict:
|
|
2157
|
+
session_id = str(params.get("session_id") or "").strip()
|
|
2158
|
+
reason = params.get("reason")
|
|
2159
|
+
if not session_id or reason not in ("clear", "delete"):
|
|
2160
|
+
raise InvalidParams("session_id and reason (clear/delete) are required")
|
|
2161
|
+
home = profile_home_param(params)
|
|
2162
|
+
messages: list[str] = []
|
|
2163
|
+
|
|
2164
|
+
def finalize() -> None:
|
|
2165
|
+
try:
|
|
2166
|
+
# Scope inside the worker: a timed-out plugin keeps its own profile until it finishes.
|
|
2167
|
+
with scoped_home(home):
|
|
2168
|
+
from hermes_cli import lifecycle
|
|
2169
|
+
|
|
2170
|
+
callback = getattr(lifecycle, "finalize_session", None)
|
|
2171
|
+
if callback is None:
|
|
2172
|
+
return
|
|
2173
|
+
results = callback(session_id=session_id, platform="webui", reason=reason)
|
|
2174
|
+
extract = getattr(lifecycle, "session_end_messages", None)
|
|
2175
|
+
if extract is not None:
|
|
2176
|
+
messages.extend(extract(results))
|
|
2177
|
+
except Exception: # noqa: BLE001 - hooks must never block the boundary
|
|
2178
|
+
log.warning("Session finalize failed for %s (%s)", session_id, reason, exc_info=True)
|
|
2179
|
+
finally:
|
|
2180
|
+
with _RELEASES_LOCK:
|
|
2181
|
+
_RELEASES.pop(worker, None)
|
|
2182
|
+
|
|
2183
|
+
worker = threading.Thread(target=finalize, name="session-finalize", daemon=True)
|
|
2184
|
+
worker.failed = False # type: ignore[attr-defined] - hook failures do not prevent profile deletion
|
|
2185
|
+
with _RELEASES_LOCK:
|
|
2186
|
+
_RELEASES[worker] = home
|
|
2187
|
+
worker.start()
|
|
2188
|
+
worker.join(_SESSION_FINALIZE_TIMEOUT)
|
|
2189
|
+
if worker.is_alive():
|
|
2190
|
+
log.warning("Session finalize exceeded %ss for %s (%s)", _SESSION_FINALIZE_TIMEOUT, session_id, reason)
|
|
2191
|
+
return {"messages": []}
|
|
2192
|
+
return {"messages": messages}
|
|
2193
|
+
|
|
1493
2194
|
@registry.method("chat.evict_agent", requires_agent=False)
|
|
1494
2195
|
def evict_(ctx: CallContext, params: dict) -> dict:
|
|
1495
2196
|
"""Drop a session's cached agent. Only delete/clear (``clear_session``) end its approval grants, parked
|
|
1496
|
-
approvals and
|
|
2197
|
+
approvals, kernels and background review; a model switch, truncate or compress leaves a running turn's agent
|
|
2198
|
+
in place."""
|
|
1497
2199
|
session_id = str(params.get("session_id") or "").strip()
|
|
1498
2200
|
end_session = params.get("clear_session") is True
|
|
1499
2201
|
with _AGENT_CACHE_LOCK:
|
|
1500
2202
|
with _RUNS_LOCK:
|
|
1501
2203
|
run = _RUNS.get(_RUNS_BY_SESSION.get(session_id) or "")
|
|
2204
|
+
if end_session and run is not None:
|
|
2205
|
+
run.session_ended = True
|
|
1502
2206
|
if not end_session and run is not None and not run.finished.is_set():
|
|
1503
2207
|
return {"evicted": False}
|
|
1504
2208
|
entry = _AGENT_CACHE.pop(session_id, None)
|
|
1505
2209
|
_release_agents_locked([entry] if entry else [])
|
|
1506
2210
|
if end_session:
|
|
2211
|
+
# The server does not wait for an eviction; the cancels finish on their own.
|
|
2212
|
+
_cancel_background_reviews(([(entry[0], entry[2])] if entry else []) + ([(run.agent, run.home)] if run else []), "session deleted or cleared")
|
|
1507
2213
|
try:
|
|
1508
2214
|
from tools.approval import clear_session
|
|
1509
2215
|
clear_session(session_id)
|
|
1510
2216
|
except Exception: # noqa: BLE001
|
|
1511
2217
|
pass
|
|
2218
|
+
# TAL-887: a password manager this session unlocked locks again with it; other sessions keep theirs.
|
|
2219
|
+
_release_vault_session(session_id)
|
|
1512
2220
|
return {"evicted": entry is not None}
|
|
1513
2221
|
|
|
1514
2222
|
@registry.method("chat.commit_memory", requires_agent=False)
|
|
@@ -1600,22 +2308,27 @@ def register(registry) -> None:
|
|
|
1600
2308
|
|
|
1601
2309
|
@registry.method("clarify.respond", requires_agent=False)
|
|
1602
2310
|
def clarify_respond_(ctx: CallContext, params: dict) -> dict:
|
|
1603
|
-
response = str(params.get("response") or "")
|
|
2311
|
+
response = str(params.get("response") or "")
|
|
1604
2312
|
clarify_id = str(params.get("clarify_id") or "").strip()
|
|
1605
|
-
if not response:
|
|
1606
|
-
raise InvalidParams("response is required")
|
|
1607
2313
|
run = _run_for(params)
|
|
1608
2314
|
if run is None:
|
|
1609
2315
|
return {"ok": False}
|
|
2316
|
+
if params.get("session_id") and str(params["session_id"]) != run.session_id:
|
|
2317
|
+
return {"ok": False}
|
|
1610
2318
|
with run.lock:
|
|
1611
2319
|
entries = list(run.clarify_entries.values())
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2320
|
+
target = next((e for e in entries if e.clarify_id == clarify_id), None) if clarify_id else (entries[0] if entries else None)
|
|
2321
|
+
if target is None or target.event.is_set() or (target.deadline is not None and time.monotonic() >= target.deadline):
|
|
2322
|
+
return {"ok": False}
|
|
2323
|
+
skill_secret = target.data.get("kind") == "skill_secret"
|
|
2324
|
+
if skill_secret and (not clarify_id or ctx.cancelled or run.ctx.cancelled or run.cancel.is_set() or run.finished.is_set() or run.session_ended):
|
|
2325
|
+
return {"ok": False}
|
|
2326
|
+
# Secure answers are as typed; an empty reply is their decline. Only the first answer releases a waiter.
|
|
2327
|
+
secure = str(target.data.get("kind") or "").startswith("vault_") or skill_secret
|
|
2328
|
+
if not secure:
|
|
2329
|
+
response = response.strip()
|
|
2330
|
+
if not response and not secure:
|
|
2331
|
+
raise InvalidParams("response is required")
|
|
2332
|
+
target.result = response
|
|
2333
|
+
target.event.set()
|
|
2334
|
+
return {"ok": True, "clarify_id": target.clarify_id}
|