@maudecode/talaria-web 1.18.0 → 1.20.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 +93 -38
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +99 -17
- package/dist/api/automation-router.js +22 -14
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1433 -37
- package/dist/api/chat-router.js +205 -90
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +6 -2
- package/dist/api/git-router.js +30 -30
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +3 -3
- package/dist/api/raw-routes.js +76 -62
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +5 -5
- 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 +2635 -151
- package/dist/api/sessions-router.js +193 -177
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +450 -11
- package/dist/api/settings-router.js +102 -49
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +2 -2
- package/dist/api/sse-routes.js +102 -80
- 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 +963 -109
- package/dist/api/tools-router.js +426 -72
- 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 +80 -57
- package/dist/app.js.map +1 -1
- package/dist/auth/crypto.d.ts +3 -0
- package/dist/auth/crypto.js +18 -0
- package/dist/auth/crypto.js.map +1 -1
- package/dist/auth/gate.js +14 -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 +8 -2
- 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 +90 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/supervise.d.ts +24 -0
- package/dist/cli/supervise.js +147 -6
- package/dist/cli/supervise.js.map +1 -1
- package/dist/config/agent-config.d.ts +37 -11
- package/dist/config/agent-config.js +146 -64
- 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 +4 -0
- package/dist/fs/atomic.js +19 -2
- package/dist/fs/atomic.js.map +1 -1
- package/dist/fs/stat.d.ts +7 -0
- package/dist/fs/stat.js +27 -0
- package/dist/fs/stat.js.map +1 -0
- package/dist/http/context.d.ts +28 -9
- package/dist/http/context.js +77 -25
- 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 +12 -4
- package/dist/profiles/profiles.js +78 -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 +85 -14
- package/dist/providers/catalog.js +502 -146
- 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 +56 -23
- package/dist/redact.js.map +1 -1
- package/dist/release.d.ts +7 -0
- package/dist/release.js +19 -3
- package/dist/release.js.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.js +303 -96
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +31 -18
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +11 -4
- package/dist/sessions/anchor.js +33 -37
- 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 +49 -23
- package/dist/sessions/background-tasks.js +161 -89
- 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 +23 -6
- package/dist/sessions/cli-sessions.js +84 -84
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +38 -2
- package/dist/sessions/completions.js +348 -77
- 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/export.d.ts +2 -0
- package/dist/sessions/export.js +6 -2
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +86 -0
- package/dist/sessions/gateway-sessions.js +914 -0
- package/dist/sessions/gateway-sessions.js.map +1 -0
- package/dist/sessions/gateway-watcher.d.ts +10 -11
- package/dist/sessions/gateway-watcher.js +25 -21
- package/dist/sessions/gateway-watcher.js.map +1 -1
- 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 +34 -18
- package/dist/sessions/list.js +161 -147
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +77 -13
- package/dist/sessions/merge.js +315 -65
- 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 +85 -0
- package/dist/sessions/prefix-index.js +243 -0
- package/dist/sessions/prefix-index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +4 -7
- package/dist/sessions/recovery.js +100 -283
- 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 +251 -78
- package/dist/sessions/service.js +1510 -564
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +20 -2
- package/dist/sessions/session.js +33 -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 +24 -5
- package/dist/sessions/state-db-worker.js.map +1 -1
- package/dist/sessions/state-db.d.ts +157 -21
- package/dist/sessions/state-db.js +1145 -204
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/state-saved.d.ts +1 -1
- package/dist/sessions/state-saved.js +35 -11
- package/dist/sessions/state-saved.js.map +1 -1
- package/dist/sessions/storage/backend.d.ts +171 -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 +25 -0
- package/dist/sessions/storage/json-documents.js +277 -0
- package/dist/sessions/storage/json-documents.js.map +1 -0
- package/dist/sessions/storage/json-files.d.ts +23 -0
- package/dist/sessions/storage/json-files.js +244 -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 +113 -0
- package/dist/sessions/storage/json-journal.js +756 -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 +82 -0
- package/dist/sessions/storage/json.js +350 -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 +67 -73
- package/dist/sessions/store.js +341 -607
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +12 -7
- package/dist/sessions/streams.js +58 -15
- 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 +8 -5
- package/dist/sessions/tool-display.js +28 -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 +13 -6
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +110 -22
- package/dist/sessions/turn.js +918 -380
- 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 +41 -4
- package/dist/sidecar/client.js +192 -42
- 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.d.ts +6 -2
- package/dist/sidecar/fake.js +34 -4
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.d.ts +9 -4
- package/dist/spa.js +36 -12
- package/dist/spa.js.map +1 -1
- package/dist/startup-timing.d.ts +66 -0
- package/dist/startup-timing.js +129 -0
- package/dist/startup-timing.js.map +1 -0
- 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/fs-handles.d.ts +11 -0
- package/dist/test/fs-handles.js +31 -0
- package/dist/test/fs-handles.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 +46 -5
- package/dist/tools/crons.js +100 -26
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +34 -5
- package/dist/tools/dashboard-plugins.js +146 -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 +82 -0
- package/dist/tools/gateway-restart.js +222 -0
- package/dist/tools/gateway-restart.js.map +1 -0
- package/dist/tools/health.d.ts +28 -4
- package/dist/tools/health.js +171 -50
- 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/insights.d.ts +1 -1
- package/dist/tools/insights.js +2 -2
- package/dist/tools/insights.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/mcp-health.js +74 -24
- package/dist/tools/mcp-health.js.map +1 -1
- package/dist/tools/mcp.js +19 -1
- package/dist/tools/mcp.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-oauth.d.ts +14 -0
- package/dist/tools/plugin-oauth.js +53 -0
- package/dist/tools/plugin-oauth.js.map +1 -0
- 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/prune-advice.d.ts +50 -0
- package/dist/tools/prune-advice.js +114 -0
- package/dist/tools/prune-advice.js.map +1 -0
- package/dist/tools/skills.d.ts +13 -5
- package/dist/tools/skills.js +53 -19
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +102 -17
- package/dist/tools/update-notifications.js +400 -82
- 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 +100 -33
- package/dist/tools/updates.js +1085 -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 +98 -38
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/preview.js +4 -14
- package/dist/workspace/preview.js.map +1 -1
- package/dist/workspace/rollback.d.ts +8 -4
- package/dist/workspace/rollback.js +22 -12
- 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 +6 -4
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +29 -0
- package/sidecar/scripts/record-fixtures.py +12 -3
- package/sidecar/scripts/replay_sidecar.py +8 -0
- package/sidecar/scripts/suite_runner.py +458 -0
- package/sidecar/scripts/test.sh +11 -5
- package/sidecar/talaria_sidecar/__init__.py +6 -1
- package/sidecar/talaria_sidecar/__main__.py +13 -4
- package/sidecar/talaria_sidecar/agent_mutation.py +1111 -0
- package/sidecar/talaria_sidecar/agent_services.py +1116 -0
- package/sidecar/talaria_sidecar/context_detail.py +59 -0
- package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
- package/sidecar/talaria_sidecar/home.py +133 -14
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +211 -41
- package/sidecar/talaria_sidecar/methods/chat.py +1378 -200
- package/sidecar/talaria_sidecar/methods/checkpoints.py +29 -4
- package/sidecar/talaria_sidecar/methods/commands.py +483 -13
- package/sidecar/talaria_sidecar/methods/config.py +195 -6
- package/sidecar/talaria_sidecar/methods/connectors.py +375 -0
- package/sidecar/talaria_sidecar/methods/cron.py +374 -36
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +301 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +215 -55
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +418 -0
- package/sidecar/talaria_sidecar/methods/goals.py +81 -41
- package/sidecar/talaria_sidecar/methods/kanban.py +82 -30
- package/sidecar/talaria_sidecar/methods/memory.py +84 -0
- package/sidecar/talaria_sidecar/methods/oauth.py +144 -13
- package/sidecar/talaria_sidecar/methods/plugin_catalog.py +198 -0
- package/sidecar/talaria_sidecar/methods/plugin_oauth.py +234 -0
- package/sidecar/talaria_sidecar/methods/process.py +229 -16
- package/sidecar/talaria_sidecar/methods/profiles.py +48 -9
- package/sidecar/talaria_sidecar/methods/providers.py +196 -26
- package/sidecar/talaria_sidecar/methods/runtime.py +71 -1
- package/sidecar/talaria_sidecar/methods/skills.py +83 -5
- package/sidecar/talaria_sidecar/methods/state_db.py +405 -112
- 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 +51 -0
- package/sidecar/talaria_sidecar/methods/usage.py +113 -14
- package/sidecar/talaria_sidecar/methods/worktree.py +145 -19
- package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
- package/sidecar/talaria_sidecar/rpc.py +70 -7
- package/sidecar/talaria_sidecar/runtime.py +122 -14
- package/sidecar/talaria_sidecar/speed.py +47 -0
- package/sidecar/talaria_sidecar/startup_timing.py +119 -0
- package/static/dist/FILES.txt +242 -215
- package/static/dist/THIRD_PARTY_LICENSES.md +6 -22
- package/static/dist/assets/AppShell-3isgvoro.js +8 -0
- package/static/dist/assets/Button-f4n6dapf.js +1 -0
- package/static/dist/assets/ChatPage-k7b6xfax.js +4 -0
- package/static/dist/assets/ConnectTargets-juqvt5lm.js +1 -0
- package/static/dist/assets/Dialog-hta5utr3.js +1 -0
- package/static/dist/assets/ExtensionRoute-zrhjn0wm.js +1 -0
- package/static/dist/assets/Field-b6ez48oz.js +1 -0
- package/static/dist/assets/HubRoute-iwvso2tz.js +2 -0
- package/static/dist/assets/LinkCheckContext-ix6zqqkk.js +1 -0
- package/static/dist/assets/Markdown-jdeth7zq.js +2 -0
- package/static/dist/assets/MarkdownRenderer-fbswsgld.js +302 -0
- package/static/dist/assets/MarkdownRenderer-i9o6nr38.css +1 -0
- package/static/dist/assets/OAuthSignIn-bpd8hom3.js +1 -0
- package/static/dist/assets/ResourceSwitch-odi6dy2o.js +1 -0
- package/static/dist/assets/RightPanel-eu6enlo6.js +1 -0
- package/static/dist/assets/Select-kr07928w.js +1 -0
- package/static/dist/assets/SessionListPanel-etb2i54l.js +1 -0
- package/static/dist/assets/SettingsLayout-eu3xxp23.js +1 -0
- package/static/dist/assets/States-icab19yg.js +1 -0
- package/static/dist/assets/TerminalPanel-hngmsr8b.js +16 -0
- package/static/dist/assets/Toaster-nb2uzjw1.js +1 -0
- package/static/dist/assets/_app-ga1moszl.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-na62087t.js +1 -0
- package/static/dist/assets/_app.index-jypgdqoi.js +1 -0
- package/static/dist/assets/_app.insights-kabejdtm.js +1 -0
- package/static/dist/assets/_app.kanban-m19kfcmx.js +1 -0
- package/static/dist/assets/_app.logs-iw4m96sz.js +1 -0
- package/static/dist/assets/_app.memory-jq5frwcg.js +1 -0
- package/static/dist/assets/_app.profiles-kwa3m7rp.js +1 -0
- package/static/dist/assets/_app.session._sessionId-o5lyeipj.js +1 -0
- package/static/dist/assets/_app.settings-ckl8hdll.js +1 -0
- package/static/dist/assets/_app.settings._section-dtdjk98a.js +3 -0
- package/static/dist/assets/_app.skills-n016rihd.js +1 -0
- package/static/dist/assets/_app.tasks-n8zuqsqe.js +1 -0
- package/static/dist/assets/_app.todos-btew5v2s.js +1 -0
- package/static/dist/assets/_app.workspaces-lnfk95ch.js +1 -0
- package/static/dist/assets/abnfDiagram-VCTEODGH-dttheegb.js +1 -0
- package/static/dist/assets/{angular-html-nu6l31w3.js → angular-html-l28bd5fo.js} +1 -1
- package/static/dist/assets/{angular-ts-fbx4f8mm.js → angular-ts-ffzx1vj0.js} +1 -1
- package/static/dist/assets/{apl-bx2xr7dh.js → apl-b7h7mmy4.js} +1 -1
- package/static/dist/assets/{arc-7popfhbk.js → arc-fk1b21lm.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-ic435919.js +1 -0
- package/static/dist/assets/architectureDiagram-5GKGNRK7-8r3glgiq.js +36 -0
- package/static/dist/assets/archive-il7c6053.js +1 -0
- package/static/dist/assets/{astro-g77i97cb.js → astro-k117fy0t.js} +1 -1
- package/static/dist/assets/back-gu3nu279.js +1 -0
- package/static/dist/assets/{blade-boo0u92y.js → blade-npefsmn8.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-jenyos6c.js} +4 -4
- package/static/dist/assets/{c-cat0w3sc.js → c-2umg6kbn.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-nzlx7yc4.js} +1 -1
- package/static/dist/assets/cancel-bsnfo36u.js +1 -0
- package/static/dist/assets/channel-dsbtk01y.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-g0cru63b.js} +20 -20
- package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-7rkswkjw.js} +1 -1
- package/static/dist/assets/chunk-DU6HZSFF-fn73jzdo.js +128 -0
- package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-oey8e72m.js} +1 -1
- package/static/dist/assets/{chunk-FOHPRMQF-opuflp73.js → chunk-FOHPRMQF-ipewusgj.js} +8 -8
- package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-l2a32wiy.js} +6 -6
- package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-o9dwzh8h.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-eblwj20b.js} +5 -5
- package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-o2mufspb.js} +1 -1
- package/static/dist/assets/chunk-OSK3NFVY-ijp9pw29.js +10 -0
- package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-hs9hqwl4.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-bp9rsqez.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-brkprov6.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-gmpv0pxz.js} +13 -13
- package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-oldm08fu.js} +4 -4
- package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-ooqilfv7.js} +6 -6
- package/static/dist/assets/{chunk-XXDRQBXY-n40pf9vy.js → chunk-XXDRQBXY-jkmnqu65.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +1 -0
- package/static/dist/assets/{cobol-go622aep.js → cobol-ssf9hkh7.js} +1 -1
- package/static/dist/assets/{coffee-jwo1xjhq.js → coffee-lsi9fwae.js} +1 -1
- package/static/dist/assets/copied-bz3gkazh.js +1 -0
- package/static/dist/assets/copy-jo006coq.js +1 -0
- package/static/dist/assets/cose-bilkent-JH36ORCC-layy2r27.js +1 -0
- package/static/dist/assets/{cpp-nre7j0kq.js → cpp-lfrh4y55.js} +1 -1
- package/static/dist/assets/{crystal-lviso2je.js → crystal-efh4pte7.js} +1 -1
- package/static/dist/assets/{css-ywz9j4hb.js → css-i82qdo89.js} +1 -1
- package/static/dist/assets/cynefin-OW5HDTMX-d47op7qs.js +1 -0
- package/static/dist/assets/cynefinDiagram-5FMLGOSQ-doa7594y.js +62 -0
- package/static/dist/assets/cytoscape.esm-fjn2algp.js +321 -0
- package/static/dist/assets/dagre-GXQ25YYZ-os768dp4.js +4 -0
- package/static/dist/assets/dagre-o5jmjz46.js +1 -0
- package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-fyi81nxd.js} +4 -4
- package/static/dist/assets/diagram-UQ7AKVKN-j3exggaq.js +41 -0
- package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-omo1en2l.js} +1 -1
- package/static/dist/assets/diagram-VX7I27RA-oro2cyu0.js +24 -0
- package/static/dist/assets/diagram-Z3DM3KII-byt9vdb7.js +24 -0
- package/static/dist/assets/dist-crc46di9.js +7 -0
- package/static/dist/assets/dist-etpoyscq.js +44 -0
- package/static/dist/assets/{dist-c4c2m3kv.js → dist-nb670eu6.js} +1 -1
- package/static/dist/assets/ebnfDiagram-PWID7BFC-ee3haugh.js +1 -0
- package/static/dist/assets/{edge-dvv35byi.js → edge-lrob635n.js} +1 -1
- package/static/dist/assets/{elixir-i2f1ki1g.js → elixir-kdpxeq93.js} +1 -1
- package/static/dist/assets/{elm-cpepyjm4.js → elm-c8irj9uh.js} +1 -1
- package/static/dist/assets/erDiagram-RLTQ6QDP-d1e93yff.js +99 -0
- package/static/dist/assets/{erb-idlyfheg.js → erb-flwsv146.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-i1ihb40c.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +1 -0
- package/static/dist/assets/folder-plus-ivoglq98.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-m7g0jud4.js} +1 -1
- package/static/dist/assets/{git-rebase-d9lpfpbn.js → git-rebase-bli19aoq.js} +1 -1
- package/static/dist/assets/gitGraph-4MIJSDKK-hzdj07yy.js +1 -0
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-dm9agbna.js} +1 -1
- package/static/dist/assets/{glimmer-js-39i0urfb.js → glimmer-js-d4icg38p.js} +1 -1
- package/static/dist/assets/{glimmer-ts-gdtd6n73.js → glimmer-ts-bk75772u.js} +1 -1
- package/static/dist/assets/{glsl-o1djm5pp.js → glsl-lqormp2b.js} +1 -1
- package/static/dist/assets/{graphql-kj6rz55t.js → graphql-hw6s5qzd.js} +1 -1
- package/static/dist/assets/{hack-pe1qqr2i.js → hack-ozrv6l4d.js} +1 -1
- package/static/dist/assets/{haml-cknnbcqs.js → haml-gytfosdt.js} +1 -1
- package/static/dist/assets/{handlebars-hwq64orh.js → handlebars-b6g7jffd.js} +1 -1
- package/static/dist/assets/highlighted-body-KPVGNVTW-mmgz2czu.js +1 -0
- package/static/dist/assets/host-fr6lni1p.js +1 -0
- package/static/dist/assets/{html-derivative-h2i2pyjc.js → html-derivative-f1tqcroo.js} +1 -1
- package/static/dist/assets/{html-ha05xtvt.js → html-on6oj6yj.js} +1 -1
- package/static/dist/assets/{http-cf94zu27.js → http-odhufsl7.js} +1 -1
- package/static/dist/assets/{hurl-lslax568.js → hurl-mu680336.js} +1 -1
- package/static/dist/assets/index-73649w7o.js +10 -0
- package/static/dist/assets/index-lhf1fsdf.css +2 -0
- package/static/dist/assets/info-A6RAGUB7-lll1dqmm.js +1 -0
- package/static/dist/assets/infoDiagram-27XIBGKW-k7esc639.js +2 -0
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-lgr6k9wo.js} +6 -6
- package/static/dist/assets/{java-ndjf2o89.js → java-nxbe8uju.js} +1 -1
- package/static/dist/assets/{javascript-coftlwy3.js → javascript-lz3kxcyz.js} +1 -1
- package/static/dist/assets/{jinja-etei9wy7.js → jinja-l5pk84gt.js} +1 -1
- package/static/dist/assets/{jison-hyekgft1.js → jison-jdutdwyy.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-ndmbxuqk.js} +1 -1
- package/static/dist/assets/{json-lok0yaho.js → json-dnge4ssf.js} +1 -1
- package/static/dist/assets/{jsx-b64w0nbx.js → jsx-c3lwhkep.js} +1 -1
- package/static/dist/assets/{jsx-runtime-eu92a5pu.js → jsx-runtime-o40ukmjl.js} +1 -1
- package/static/dist/assets/{julia-jxxzz2yn.js → julia-hwd4rrcb.js} +1 -1
- package/static/dist/assets/{just-mc29iw9t.js → just-nka3oxdx.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-mjxym0jp.js} +28 -28
- package/static/dist/assets/katex-cbdd1ohw.css +1 -0
- package/static/dist/assets/katex-pbij2oo6.js +257 -0
- package/static/dist/assets/{latex-pafa03ft.js → latex-iqmr7e40.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-cwb6l7xh.js +1 -0
- package/static/dist/assets/{line-meizqsvn.js → line-mtr5k6il.js} +1 -1
- package/static/dist/assets/linear-jlwrvvy6.js +1 -0
- package/static/dist/assets/link-iadw4ghw.js +2 -0
- package/static/dist/assets/{liquid-hz5c2pov.js → liquid-okmk9tbp.js} +1 -1
- package/static/dist/assets/locale-cs-n41hptse.js +5 -0
- package/static/dist/assets/locale-de-k7ht47ew.js +5 -0
- package/static/dist/assets/locale-es-ih078lco.js +5 -0
- package/static/dist/assets/locale-fr-ojabnf41.js +5 -0
- package/static/dist/assets/locale-it-kb96jufy.js +5 -0
- package/static/dist/assets/locale-ja-jpsp3xs1.js +7 -0
- package/static/dist/assets/locale-ko-cpzuy0n7.js +5 -0
- package/static/dist/assets/locale-pl-dbap4leu.js +5 -0
- package/static/dist/assets/locale-pt-nci7w72x.js +5 -0
- package/static/dist/assets/locale-ru-soghe4cs.js +7 -0
- package/static/dist/assets/locale-tr-ks00mu68.js +5 -0
- package/static/dist/assets/locale-vi-ij7twfm6.js +5 -0
- package/static/dist/assets/locale-zh-Hant-feakwr7a.js +5 -0
- package/static/dist/assets/locale-zh-ren0vjyv.js +7 -0
- package/static/dist/assets/login-lkhtzfcl.js +1 -0
- package/static/dist/assets/{lua-leyf8mlb.js → lua-krx4xj5h.js} +1 -1
- package/static/dist/assets/{marko-cn1fzpdv.js → marko-gbvpfehz.js} +1 -1
- package/static/dist/assets/{mdc-huhy88df.js → mdc-c21zlyi5.js} +1 -1
- package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +1 -0
- package/static/dist/assets/mermaid-parser.core-k91vdnvi.js +7 -0
- package/static/dist/assets/micromark-factory-space-fnmnpi6s.js +1 -0
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-ehztfea4.js} +28 -28
- package/static/dist/assets/{nginx-e7lvhsmn.js → nginx-f3d68ed4.js} +1 -1
- package/static/dist/assets/{nim-dun5d8mk.js → nim-dw0jcy70.js} +1 -1
- package/static/dist/assets/onboarding-f67z0a2r.js +1 -0
- package/static/dist/assets/packet-AYTQ26CC-ok2xrblz.js +1 -0
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/pegDiagram-XKGWAZYB-ec3rpj0m.js +1 -0
- package/static/dist/assets/{perl-bj3oqtet.js → perl-3c0r45c8.js} +1 -1
- package/static/dist/assets/{php-buzp12r0.js → php-rznhidvp.js} +1 -1
- package/static/dist/assets/pie-WAS4IAKB-iwkir23u.js +1 -0
- package/static/dist/assets/pieDiagram-E7YTZNPT-fcpqpekh.js +39 -0
- package/static/dist/assets/{pug-mjad7he9.js → pug-f2le4f3y.js} +1 -1
- package/static/dist/assets/{qml-d1gdp10s.js → qml-e2wbq9cr.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-lqsync43.js} +1 -1
- package/static/dist/assets/queryKeys-ed4n2egc.js +9 -0
- package/static/dist/assets/{r-o3r9c7ao.js → r-epfzuw6q.js} +1 -1
- package/static/dist/assets/radar-RG4KPBEZ-hje4jbk6.js +1 -0
- package/static/dist/assets/railroad-74A4TZTK-gll9n6qi.js +1 -0
- package/static/dist/assets/railroad-abnf-HS5TGJTU-o6h6lpn4.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-juaefj6o.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-cgfhvpah.js +1 -0
- package/static/dist/assets/railroadDiagram-O6MQD6OU-lp3w7awf.js +1 -0
- package/static/dist/assets/{razor-ilc585yr.js → razor-pea1u1d0.js} +1 -1
- package/static/dist/assets/refresh-cw-iccm7a4e.js +1 -0
- package/static/dist/assets/refresh-i9ya9dvl.js +1 -0
- package/static/dist/assets/{regexp-mpjtcvwm.js → regexp-hzte0a5x.js} +1 -1
- package/static/dist/assets/registry-g7w3353u.js +1 -0
- package/static/dist/assets/requirementDiagram-BXWQKSXE-ha8eibcf.js +84 -0
- package/static/dist/assets/rolldown-runtime-bnnw9q4a.js +1 -0
- package/static/dist/assets/{rst-jrkwjdyi.js → rst-m6tugj5o.js} +1 -1
- package/static/dist/assets/{ruby-n4tjcbpz.js → ruby-l19vq3w1.js} +1 -1
- package/static/dist/assets/runtime-nlxpnegr.js +1 -0
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-oxf31pr9.js} +1 -1
- package/static/dist/assets/{sas-hjo6y93k.js → sas-ktdxo397.js} +1 -1
- package/static/dist/assets/{scss-ncidjv48.js → scss-kn3oxfwf.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-jiq8lhyo.js} +11 -11
- package/static/dist/assets/settings_section_extensions_title-fadc7o65.js +1 -0
- package/static/dist/assets/share._token-o76ki0ly.js +2 -0
- package/static/dist/assets/{shellscript-m8whjpqd.js → shellscript-l100jtrg.js} +1 -1
- package/static/dist/assets/{shellsession-i5bj6syz.js → shellsession-m5qukwow.js} +1 -1
- package/static/dist/assets/sizeCapture-INFHLROL-dk1io13w.js +1 -0
- package/static/dist/assets/sliders-horizontal-ddcs88av.js +1 -0
- package/static/dist/assets/{soy-hhh36ysq.js → soy-mexlscur.js} +1 -1
- package/static/dist/assets/{sql-j21bzcl6.js → sql-o0ho5suh.js} +1 -1
- package/static/dist/assets/src-h89s0hs4.js +1 -0
- package/static/dist/assets/{stata-bbu2onh1.js → stata-be9ck1q3.js} +1 -1
- package/static/dist/assets/stateDiagram-D77RDMKH-f3fslnon.js +1 -0
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +1 -0
- package/static/dist/assets/{surrealql-l5vczuco.js → surrealql-gk1xkkyy.js} +1 -1
- package/static/dist/assets/{svelte-idn422g6.js → svelte-k6ecjilf.js} +1 -1
- package/static/dist/assets/swimlanes-42K2YHIH-k1hmk7jf.js +1 -0
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +8 -0
- package/static/dist/assets/{templ-e22qou3c.js → templ-gf3bftk6.js} +1 -1
- package/static/dist/assets/{tex-deqpc5ee.js → tex-d21fqv1z.js} +1 -1
- package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-n3ncfy9r.js} +8 -8
- package/static/dist/assets/toast-liparz2j.js +56 -0
- package/static/dist/assets/treeView-Q6P3EWNA-l3y2pejt.js +1 -0
- package/static/dist/assets/treemap-WGGIJYW6-jown9axh.js +1 -0
- package/static/dist/assets/{ts-tags-ctnibc27.js → ts-tags-n0g61gr6.js} +1 -1
- package/static/dist/assets/{tsx-i163n32g.js → tsx-cvv7wv6q.js} +1 -1
- package/static/dist/assets/{twig-x0fi7wm8.js → twig-n1f67204.js} +1 -1
- package/static/dist/assets/{typescript-g18c34xz.js → typescript-og7o55cx.js} +1 -1
- package/static/dist/assets/untitled-mj86r4gz.js +1 -0
- package/static/dist/assets/useDebounced-g8r0ggve.js +1 -0
- package/static/dist/assets/useForm-o4uu74mx.js +1 -0
- package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-ftsveu84.js} +1 -1
- package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-crqueqga.js} +1 -1
- package/static/dist/assets/vennDiagram-4TSXK5OY-dh59uq1c.js +34 -0
- package/static/dist/assets/{vue-mwu5l1bb.js → vue-ej4xpwqw.js} +1 -1
- package/static/dist/assets/{vue-html-nskj3du0.js → vue-html-d48xsjuo.js} +1 -1
- package/static/dist/assets/{vue-vine-cqe77smq.js → vue-vine-pev88ip6.js} +1 -1
- package/static/dist/assets/wardley-WFR3VGLG-e6d74b0j.js +1 -0
- package/static/dist/assets/wardleyDiagram-VM6X3IG4-hnofh78f.js +78 -0
- package/static/dist/assets/with-selector-nu1b5z0v.js +1 -0
- package/static/dist/assets/{xml-gau7gvrv.js → xml-ifxp6gqb.js} +1 -1
- package/static/dist/assets/{xsl-ew09zq8b.js → xsl-fug2cckc.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-bpymjelb.js} +6 -6
- package/static/dist/assets/{yaml-ofnrxtvf.js → yaml-fjmafnlo.js} +1 -1
- package/static/dist/extension-sdk.js +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +180 -136
- 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/Markdown-lwviulmg.css +0 -1
- 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.ext._extensionId-gfbzpoou.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.session._sessionId-fn1bhde6.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/abnfDiagram-VCTEODGH-c8kdixke.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
- package/static/dist/assets/architectureDiagram-5GKGNRK7-cku06x1p.js +0 -36
- 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/chunk-DU6HZSFF-ofajdixh.js +0 -128
- package/static/dist/assets/chunk-OSK3NFVY-e7gkikcw.js +0 -10
- 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/copy-magtofyp.js +0 -1
- package/static/dist/assets/cose-bilkent-JH36ORCC-eclcy6ja.js +0 -1
- package/static/dist/assets/cynefin-OW5HDTMX-siqlulkl.js +0 -1
- package/static/dist/assets/cynefinDiagram-5FMLGOSQ-i648tah9.js +0 -62
- package/static/dist/assets/cytoscape.esm-bqu53u12.js +0 -321
- package/static/dist/assets/dagre-GXQ25YYZ-mwn6uc17.js +0 -4
- package/static/dist/assets/dagre-lkoknbjn.js +0 -1
- package/static/dist/assets/diagram-UQ7AKVKN-mekq7zgq.js +0 -41
- package/static/dist/assets/diagram-VX7I27RA-xe6r5qfw.js +0 -24
- package/static/dist/assets/diagram-Z3DM3KII-nq07dg7k.js +0 -24
- package/static/dist/assets/ebnfDiagram-PWID7BFC-c62ipm3g.js +0 -1
- package/static/dist/assets/endpoints-klbxod4t.js +0 -64
- package/static/dist/assets/erDiagram-RLTQ6QDP-h39kmnzy.js +0 -99
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
- package/static/dist/assets/gitGraph-4MIJSDKK-fr2i7caa.js +0 -1
- package/static/dist/assets/highlighted-body-KPVGNVTW-jwpop9p8.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/info-A6RAGUB7-h11x0dxo.js +0 -1
- package/static/dist/assets/infoDiagram-27XIBGKW-dfh47dw9.js +0 -2
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
- package/static/dist/assets/linear-becwlnhk.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/mermaid-parser.core-ntne45i9.js +0 -7
- package/static/dist/assets/not-found-ccnnac39.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/packet-AYTQ26CC-l6jdka8p.js +0 -1
- package/static/dist/assets/pegDiagram-XKGWAZYB-g1er3bxj.js +0 -1
- package/static/dist/assets/pie-WAS4IAKB-i0ylcrt9.js +0 -1
- package/static/dist/assets/pieDiagram-E7YTZNPT-fbz1e55z.js +0 -39
- package/static/dist/assets/radar-RG4KPBEZ-f9b70aa4.js +0 -1
- package/static/dist/assets/railroad-74A4TZTK-lgsuoeda.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/railroadDiagram-O6MQD6OU-eqpy9fnz.js +0 -1
- package/static/dist/assets/refresh-abtu93ke.js +0 -1
- package/static/dist/assets/refresh-cw-hlwb02qv.js +0 -1
- package/static/dist/assets/requirementDiagram-BXWQKSXE-ogf6puhi.js +0 -84
- package/static/dist/assets/rolldown-runtime-b9miacsj.js +0 -1
- package/static/dist/assets/root-mbklpuv4.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/sizeCapture-INFHLROL-g612l48j.js +0 -1
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
- package/static/dist/assets/src-hjyib4mu.js +0 -1
- package/static/dist/assets/sse-iwku0sjo.js +0 -1
- package/static/dist/assets/stateDiagram-D77RDMKH-fyoznpcs.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
- package/static/dist/assets/swimlanes-42K2YHIH-oefldhfy.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/treeView-Q6P3EWNA-f7hcci5o.js +0 -1
- package/static/dist/assets/treemap-WGGIJYW6-nl85071a.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
- package/static/dist/assets/vennDiagram-4TSXK5OY-h5hzbk0n.js +0 -34
- package/static/dist/assets/wardley-WFR3VGLG-jftvpm3e.js +0 -1
- package/static/dist/assets/wardleyDiagram-VM6X3IG4-hedh85ge.js +0 -78
- /package/static/dist/assets/{ChatPage-gmu5c7lf.css → TerminalPanel-gmu5c7lf.css} +0 -0
|
@@ -14,21 +14,29 @@ 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 queue
|
|
23
|
+
import re
|
|
21
24
|
import threading
|
|
22
25
|
import time
|
|
23
26
|
import uuid
|
|
24
27
|
from collections import OrderedDict
|
|
28
|
+
from functools import lru_cache
|
|
25
29
|
from pathlib import Path
|
|
26
|
-
from typing import Any
|
|
30
|
+
from typing import Any, Callable
|
|
31
|
+
from types import SimpleNamespace
|
|
27
32
|
|
|
28
33
|
from ..errors import InvalidParams, RpcError
|
|
29
|
-
from ..home import profile_home_param, scoped_home
|
|
34
|
+
from ..home import profile_home_param, scoped_home, session_workspace
|
|
30
35
|
from ..rpc import CallContext
|
|
31
36
|
from .checkpoints import checkpoint_kwargs
|
|
37
|
+
from .providers import runtime_failure
|
|
38
|
+
from .state_db import release_all, release_home, release_session_db
|
|
39
|
+
from .toolsets import disabled_toolsets
|
|
32
40
|
|
|
33
41
|
log = logging.getLogger("talaria_sidecar.chat")
|
|
34
42
|
|
|
@@ -37,10 +45,10 @@ _AGENT_CACHE_MAX = 32
|
|
|
37
45
|
_AGENT_RELEASE_TIMEOUT = 4.0
|
|
38
46
|
# Profile deletion waits this long for the profile's memory flushes, under the server's 120 s call timeout.
|
|
39
47
|
_PROFILE_RELEASE_TIMEOUT = 60.0
|
|
48
|
+
_SESSION_FINALIZE_TIMEOUT = 10.0
|
|
40
49
|
_TOOL_RESULT_SNIPPET_MAX = 4000
|
|
41
50
|
# Predecessor ``_TOOL_ARG_CONTENT_KEYS`` (#4928): card content / diff-reconstruction inputs keep the long cap.
|
|
42
51
|
_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
52
|
|
|
45
53
|
|
|
46
54
|
class _Run:
|
|
@@ -56,6 +64,11 @@ class _Run:
|
|
|
56
64
|
self.lock = threading.Lock()
|
|
57
65
|
# The Agent's transcript list from before this turn: a Stop while the Agent still holds it has no checkpoint.
|
|
58
66
|
self.prior_messages = None
|
|
67
|
+
# TAL-897: the session ended (clear/delete) or the sidecar is draining, set under both cache and run locks; an
|
|
68
|
+
# agent this run binds afterwards forks no review and stays out of the cache.
|
|
69
|
+
self.session_ended = False
|
|
70
|
+
self.home = None # the profile home its agent runs under, set by ``start``
|
|
71
|
+
self.mcp_discovery_outcome = "ok"
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
class _ClarifyEntry:
|
|
@@ -64,6 +77,7 @@ class _ClarifyEntry:
|
|
|
64
77
|
self.data = data
|
|
65
78
|
self.event = threading.Event()
|
|
66
79
|
self.result: str | None = None
|
|
80
|
+
self.deadline: float | None = None
|
|
67
81
|
|
|
68
82
|
|
|
69
83
|
_RUNS: dict[str, _Run] = {}
|
|
@@ -76,13 +90,22 @@ _RUNS_LOCK = threading.Lock()
|
|
|
76
90
|
# session id -> (agent, signature, profile home).
|
|
77
91
|
_AGENT_CACHE: "OrderedDict[str, tuple[Any, str, Any]]" = OrderedDict()
|
|
78
92
|
_AGENT_CACHE_LOCK = threading.Lock()
|
|
79
|
-
# Running release
|
|
80
|
-
_RELEASES: dict[threading.Thread, Any] = {}
|
|
93
|
+
# Running or queued release work -> the profile home it holds.
|
|
94
|
+
_RELEASES: dict[threading.Thread | _ArchiveCallback, Any] = {}
|
|
81
95
|
_RELEASES_LOCK = threading.Lock()
|
|
82
|
-
|
|
96
|
+
_ARCHIVE_MAX_WORKERS = 4
|
|
97
|
+
_ARCHIVE_QUEUE: queue.Queue[_ArchiveCallback | None] = queue.Queue(maxsize=256)
|
|
98
|
+
_ARCHIVE_WORKERS: list[threading.Thread] = []
|
|
99
|
+
# Non-turn ``(agent, home)`` holders for memory commits, refine, context reads and MCP refresh (under
|
|
100
|
+
# ``_AGENT_CACHE_LOCK``): each defers the agent's release, and deleting ``home`` waits for it.
|
|
83
101
|
_COMMITTING: list = []
|
|
84
102
|
# Set (under ``_RUNS_LOCK``) when shutdown drains: no turn is admitted after the drain's snapshot of ``_RUNS``.
|
|
85
103
|
_DRAINING = threading.Event()
|
|
104
|
+
_MCP_DISCOVERIES: dict[str, threading.Event] = {}
|
|
105
|
+
_MCP_DISCOVERY_LOCK = threading.Lock()
|
|
106
|
+
_MCP_CLOSING_HOMES: set[str] = set()
|
|
107
|
+
_MCP_REDISCOVERY_WAIT = 2.0
|
|
108
|
+
_MCP_PENDING_KEY = "mcp_discovery_pending"
|
|
86
109
|
|
|
87
110
|
|
|
88
111
|
def _snippet(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> str:
|
|
@@ -101,7 +124,17 @@ def _snippet(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> str:
|
|
|
101
124
|
|
|
102
125
|
|
|
103
126
|
_RAW_RESULT_MAX_KEYS = 64
|
|
104
|
-
_RAW_RESULT_OUTCOME_KEYS = ("error", "exit_code", "exitCode", "success")
|
|
127
|
+
_RAW_RESULT_OUTCOME_KEYS = ("error", "exit_code", "exitCode", "success", "status", "results")
|
|
128
|
+
# TAL-1261: a ``results`` batch keeps up to 64 items, each projected like the result with 16 fields of 400 characters.
|
|
129
|
+
_RAW_RESULT_ITEM_KEYS = 16
|
|
130
|
+
_RAW_RESULT_ITEM_MAX = 400
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _without_error(item: Any) -> bool:
|
|
134
|
+
"""An item the server's ``presentError`` reads as error-free: not an object, or its ``error`` is blank text, ``false``,
|
|
135
|
+
``null``, a number, or an empty object or list."""
|
|
136
|
+
error = item.get("error") if isinstance(item, dict) else None
|
|
137
|
+
return not (error is True or (isinstance(error, str) and error.strip()) or (isinstance(error, (dict, list)) and error))
|
|
105
138
|
|
|
106
139
|
|
|
107
140
|
def _result_diff(raw: Any) -> str | None:
|
|
@@ -114,10 +147,36 @@ def _result_diff(raw: Any) -> str | None:
|
|
|
114
147
|
return diff if isinstance(diff, str) else None
|
|
115
148
|
|
|
116
149
|
|
|
117
|
-
def
|
|
150
|
+
def _edit_snapshot(name: str, args: Any, task_id: str) -> Any:
|
|
151
|
+
"""TAL-909: the Agent's before-state of a file edit, through the task's terminal; None for a remote backend (the
|
|
152
|
+
Agent never diffs an unrelated local file), a non-edit tool, or any failure."""
|
|
153
|
+
try:
|
|
154
|
+
from agent.display import capture_local_edit_snapshot
|
|
155
|
+
|
|
156
|
+
return capture_local_edit_snapshot(name, args, task_id=task_id)
|
|
157
|
+
except Exception: # noqa: BLE001
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _edit_metadata(name: str, args: Any, result: Any, snapshot: Any) -> dict:
|
|
162
|
+
"""TAL-909: a tool-result row's ``display_metadata``: an edit's unified ``diff`` from its snapshot, which the server
|
|
163
|
+
turns into ``edit_diff``. Empty when the result carries its own ``diff`` (``patch``), which the server already reads."""
|
|
164
|
+
if snapshot is None or _result_diff(result) is not None:
|
|
165
|
+
return {}
|
|
166
|
+
try:
|
|
167
|
+
from agent.display import extract_edit_diff
|
|
168
|
+
|
|
169
|
+
diff = extract_edit_diff(name, result, function_args=args, snapshot=snapshot)
|
|
170
|
+
except Exception: # noqa: BLE001
|
|
171
|
+
return {}
|
|
172
|
+
return {"diff": diff} if diff else {}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _raw_result(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX, max_keys: int = _RAW_RESULT_MAX_KEYS, items: bool = True) -> Any:
|
|
118
176
|
"""The tool result as the server's outcome rule reads it, bounded: a dict (or JSON-object text) keeps its first
|
|
119
|
-
``
|
|
120
|
-
|
|
177
|
+
``max_keys`` top-level fields plus its outcome fields, scalars as they are, text and non-empty nested values as capped
|
|
178
|
+
(JSON) text, except a ``results`` list, whose object items keep the same shape one level down (TAL-1261); anything
|
|
179
|
+
else is the capped text. The sidecar decides nothing; the server does."""
|
|
121
180
|
try:
|
|
122
181
|
data = raw if isinstance(raw, dict) else json.loads(str(raw or ""))
|
|
123
182
|
except Exception: # noqa: BLE001
|
|
@@ -126,9 +185,15 @@ def _raw_result(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> Any:
|
|
|
126
185
|
return str(raw if raw is not None else "")[:limit]
|
|
127
186
|
out: dict = {}
|
|
128
187
|
# The server's outcome rule reads these, wherever they sit in the result.
|
|
129
|
-
kept = list(data.items())[:
|
|
188
|
+
kept = list(data.items())[:max_keys] + [(k, data[k]) for k in _RAW_RESULT_OUTCOME_KEYS if k in data]
|
|
130
189
|
for key, value in kept:
|
|
131
|
-
if
|
|
190
|
+
if items and key == "results" and isinstance(value, list) and value:
|
|
191
|
+
# Past the cap, the first item without an error rides along, so a long batch is judged like the whole one.
|
|
192
|
+
batch = value[:_RAW_RESULT_MAX_KEYS] + [item for item in value[_RAW_RESULT_MAX_KEYS:] if _without_error(item)][:1]
|
|
193
|
+
# A non-object item stays text, even JSON-object text: only object items count toward the batch's outcome.
|
|
194
|
+
out[key] = [_raw_result(item, _RAW_RESULT_ITEM_MAX, _RAW_RESULT_ITEM_KEYS, False) if isinstance(item, dict)
|
|
195
|
+
else (item if isinstance(item, str) else json.dumps(item, default=str))[:_RAW_RESULT_ITEM_MAX] for item in batch]
|
|
196
|
+
elif value is None or isinstance(value, (bool, int)) or (isinstance(value, float) and value == value and abs(value) != float("inf")):
|
|
132
197
|
out[str(key)] = value
|
|
133
198
|
elif isinstance(value, (dict, list)) and not value:
|
|
134
199
|
out[str(key)] = {} if isinstance(value, dict) else []
|
|
@@ -138,6 +203,18 @@ def _raw_result(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> Any:
|
|
|
138
203
|
return out
|
|
139
204
|
|
|
140
205
|
|
|
206
|
+
_TODO_TOOL_NAMES = frozenset({"todo", "todo_list"})
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _is_todo_tool(name: Any) -> bool:
|
|
210
|
+
"""TAL-889: the Agent's own todo-tool predicate (canonical ``todo_list`` and legacy aliases); older Agents lack it."""
|
|
211
|
+
try:
|
|
212
|
+
from tools.todo_tool import is_todo_tool_name
|
|
213
|
+
except Exception: # noqa: BLE001 - older Agent without the predicate
|
|
214
|
+
return isinstance(name, str) and name in _TODO_TOOL_NAMES
|
|
215
|
+
return bool(is_todo_tool_name(name))
|
|
216
|
+
|
|
217
|
+
|
|
141
218
|
def _delegation_cost_usd(name: Any, raw: Any):
|
|
142
219
|
"""Predecessor ``_delegation_cost_usd``: total spend a ``delegate_task`` result reports, or None."""
|
|
143
220
|
if str(name or "") != "delegate_task":
|
|
@@ -255,9 +332,29 @@ def _context_length(agent: Any) -> dict:
|
|
|
255
332
|
value = getattr(cc, key, None)
|
|
256
333
|
if isinstance(value, (int, float)):
|
|
257
334
|
out[key] = int(value)
|
|
335
|
+
threshold = _next_turn_threshold(agent, cc)
|
|
336
|
+
if threshold:
|
|
337
|
+
out["next_turn_threshold_tokens"] = threshold
|
|
258
338
|
return out
|
|
259
339
|
|
|
260
340
|
|
|
341
|
+
def _next_turn_threshold(agent: Any, cc: Any) -> int | None:
|
|
342
|
+
"""TAL-1005: the compression threshold the next turn runs under. A turn that ended on a fallback leaves the
|
|
343
|
+
fallback's threshold on the compressor, but the Agent restores the primary runtime at the next turn's start
|
|
344
|
+
(``restore_primary_runtime``), so this is the trigger the compressor installs for that runtime's snapshot."""
|
|
345
|
+
value = getattr(cc, "threshold_tokens", None)
|
|
346
|
+
primary = getattr(agent, "_primary_runtime", None)
|
|
347
|
+
if getattr(agent, "_fallback_activated", False) is True and isinstance(primary, dict):
|
|
348
|
+
value = primary.get("compressor_threshold_tokens")
|
|
349
|
+
preview = getattr(cc, "preview_threshold_tokens", None)
|
|
350
|
+
if callable(preview) and isinstance(primary.get("compressor_context_length"), int):
|
|
351
|
+
try:
|
|
352
|
+
value = preview(str(primary.get("compressor_model") or ""), primary["compressor_context_length"], str(primary.get("compressor_provider") or ""))
|
|
353
|
+
except Exception: # noqa: BLE001
|
|
354
|
+
log.debug("primary threshold preview failed", exc_info=True)
|
|
355
|
+
return int(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0 else None
|
|
356
|
+
|
|
357
|
+
|
|
261
358
|
# TAL-539: the hard cap on raw tool output kept in a compressed context (predecessor ``_hard_prune_post_compression_*``).
|
|
262
359
|
_TAIL_TOOL_BUDGET_TOKENS = 4096
|
|
263
360
|
_TAIL_TOOL_MIN_SNIPPET_TOKENS = 256
|
|
@@ -280,25 +377,111 @@ def _rough_tokens(text: str) -> int:
|
|
|
280
377
|
return -(-len(text) // _ROUGH_TOKEN_CHARS)
|
|
281
378
|
|
|
282
379
|
|
|
380
|
+
def _tool_failure_outcome(text: str) -> str:
|
|
381
|
+
"""`` FAILED: <error>`` for a JSON tool payload with a top-level ``error`` / ``success: false``, `` FAILED: exit N`` for
|
|
382
|
+
a non-zero ``exit_code`` / ``exitCode`` the agent did not kill itself, else ``""``: the Agent's compression-stub rule
|
|
383
|
+
(``_payload_failure_suffix``, ``_sum_process_manage``). Payloads put these fields after ``output``, so a cut drops them."""
|
|
384
|
+
try:
|
|
385
|
+
payload = json.loads(text)
|
|
386
|
+
except (ValueError, RecursionError):
|
|
387
|
+
return ""
|
|
388
|
+
if not isinstance(payload, dict):
|
|
389
|
+
return ""
|
|
390
|
+
error = payload.get("error")
|
|
391
|
+
if not error and payload.get("success") is not False:
|
|
392
|
+
code = payload.get("exit_code")
|
|
393
|
+
code = payload.get("exitCode") if code is None else code
|
|
394
|
+
if not isinstance(code, int) or isinstance(code, bool) or code == 0 or payload.get("completion_reason") == "killed":
|
|
395
|
+
return ""
|
|
396
|
+
error = f"exit {code}"
|
|
397
|
+
preview = " ".join(str(error).split())[:80] if error else ""
|
|
398
|
+
return f" FAILED: {preview}" if preview else " FAILED"
|
|
399
|
+
|
|
400
|
+
|
|
283
401
|
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
|
-
|
|
402
|
+
"""The kept head of an over-budget tool result plus a note on what the model no longer sees, ending on the outcome.
|
|
403
|
+
TAL-827: the Agent's elision marker marks the cut, so its dispatch guard rejects a model that copies the marker
|
|
404
|
+
into a durable write; Talaria's note follows on its own line."""
|
|
405
|
+
outcome = _tool_failure_outcome(text)
|
|
406
|
+
try:
|
|
407
|
+
from agent.compression_marker import elide_middle
|
|
408
|
+
except ImportError: # An Agent without the shared marker keeps Talaria's own.
|
|
409
|
+
prefix = "[Talaria context budget: omitted "
|
|
410
|
+
suffix = f" (~{tokens} rough tokens) from this tool result; the full output remains in the visible transcript.{outcome}]"
|
|
411
|
+
note_only, widest = f"{prefix}{len(text)} chars{suffix}", f"{prefix}{len(text)} of {len(text)} chars{suffix}"
|
|
412
|
+
|
|
413
|
+
def cut(snippet: str) -> str:
|
|
414
|
+
return f"{snippet}\n\n{prefix}{len(text) - len(snippet)} of {len(text)} chars{suffix}"
|
|
415
|
+
else:
|
|
416
|
+
note = f"\n[Talaria context budget: this tool result is ~{tokens} rough tokens; the full output remains in the visible transcript.{outcome}]"
|
|
417
|
+
# With nothing kept the marker counts every char, its widest form for this text.
|
|
418
|
+
note_only = widest = elide_middle(text, 0, 0) + note
|
|
419
|
+
|
|
420
|
+
def cut(snippet: str) -> str:
|
|
421
|
+
return elide_middle(text, len(snippet), 0) + note
|
|
287
422
|
if keep_tokens < _TAIL_TOOL_MIN_SNIPPET_TOKENS:
|
|
288
|
-
return
|
|
289
|
-
room = keep_tokens - _rough_tokens(
|
|
423
|
+
return note_only
|
|
424
|
+
room = keep_tokens - _rough_tokens(widest) - 1
|
|
290
425
|
snippet = text[: len(text) * room // tokens]
|
|
291
426
|
# Density varies along the text: shrink until the kept head fits the remaining tokens.
|
|
292
427
|
while snippet and _rough_tokens(snippet) > room:
|
|
293
428
|
snippet = snippet[: len(snippet) * 3 // 4]
|
|
294
429
|
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}"
|
|
430
|
+
return cut(snippet) if snippet else note_only
|
|
298
431
|
|
|
299
432
|
|
|
300
|
-
def
|
|
301
|
-
"""
|
|
433
|
+
def _skill_result_stubs(messages: list) -> dict[int, str]:
|
|
434
|
+
"""Recovery metadata for paired skill loads, captured before the Agent can prune their success/failure payloads.
|
|
435
|
+
Extends the Agent's call-id and marker helpers; its older summaries omit small-skill markers and failure verdicts."""
|
|
436
|
+
try:
|
|
437
|
+
from agent.context_compressor import _tool_calls_by_id
|
|
438
|
+
except ImportError:
|
|
439
|
+
return {}
|
|
440
|
+
try:
|
|
441
|
+
from agent.context_compressor import _skill_pruned_marker
|
|
442
|
+
except ImportError:
|
|
443
|
+
def _skill_pruned_marker(name: str) -> str:
|
|
444
|
+
return f"[SKILL_PRUNED: content lost in compression; reload with skill_view(name='{name}')]"
|
|
445
|
+
|
|
446
|
+
calls = _tool_calls_by_id(messages)
|
|
447
|
+
stubs = {}
|
|
448
|
+
for i, msg in enumerate(messages):
|
|
449
|
+
if msg.get("role") != "tool" or not msg.get("tool_call_id"):
|
|
450
|
+
continue
|
|
451
|
+
tool, raw_args = calls.get(msg["tool_call_id"], (None, None))
|
|
452
|
+
if tool != "skill_view":
|
|
453
|
+
continue
|
|
454
|
+
try:
|
|
455
|
+
args = json.loads(raw_args)
|
|
456
|
+
except (TypeError, ValueError, RecursionError):
|
|
457
|
+
continue
|
|
458
|
+
name = args.get("name") if isinstance(args, dict) else None
|
|
459
|
+
if not isinstance(name, str) or not name or args.get("file_path"):
|
|
460
|
+
continue
|
|
461
|
+
text = _content_text(msg.get("content"))
|
|
462
|
+
outcome = _tool_failure_outcome(text)
|
|
463
|
+
if text.startswith("[skill_view] ") and " FAILED" in text.split(" (", 1)[0]:
|
|
464
|
+
outcome = " FAILED"
|
|
465
|
+
if outcome:
|
|
466
|
+
stubs[i] = f"[skill_view] name={name}{outcome}"
|
|
467
|
+
continue
|
|
468
|
+
try:
|
|
469
|
+
payload = json.loads(text)
|
|
470
|
+
except (ValueError, RecursionError):
|
|
471
|
+
payload = None
|
|
472
|
+
successful = isinstance(payload, dict) and payload.get("success") is True and isinstance(payload.get("content"), str)
|
|
473
|
+
marker = _skill_pruned_marker(name)
|
|
474
|
+
# Lean-tail stubs predate the current cap and may have been persisted by a different supported Agent.
|
|
475
|
+
demoted = text.startswith(("[skill_view output demoted at compaction ", "[tool output demoted at compaction ")) and text.endswith("] " + marker)
|
|
476
|
+
if successful or text.startswith(f"[skill_view] name={name} (") or text == marker or demoted:
|
|
477
|
+
stubs[i] = marker
|
|
478
|
+
return stubs
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _cap_tool_results(messages: list, compressor: Any, original_messages: list | None = None) -> list:
|
|
482
|
+
"""Newest bodies spend one shared budget. Skill reload/failure stubs, like omission notes, survive exhaustion."""
|
|
483
|
+
originals = messages if original_messages is None else original_messages
|
|
484
|
+
skill_stubs = _skill_result_stubs(originals)
|
|
302
485
|
budget = getattr(compressor, "tail_token_budget", None)
|
|
303
486
|
budget = budget if isinstance(budget, int) and budget > 0 else _TAIL_TOOL_BUDGET_TOKENS
|
|
304
487
|
threshold = getattr(compressor, "threshold_tokens", None)
|
|
@@ -312,13 +495,17 @@ def _cap_tool_results(messages: list, compressor: Any) -> list:
|
|
|
312
495
|
if msg.get("role") != "tool":
|
|
313
496
|
continue
|
|
314
497
|
text = _content_text(msg.get("content"))
|
|
498
|
+
stub = skill_stubs.get(i)
|
|
499
|
+
if stub and text != _content_text(originals[i].get("content")):
|
|
500
|
+
text = stub
|
|
501
|
+
out[i] = {**msg, "content": text}
|
|
315
502
|
if not text.strip():
|
|
316
503
|
continue
|
|
317
504
|
tokens = _rough_tokens(text)
|
|
318
505
|
if tokens <= budget - spent:
|
|
319
506
|
spent += tokens
|
|
320
507
|
continue
|
|
321
|
-
out[i] = {**msg, "content": _capped_tool_result(text, tokens, budget - spent)}
|
|
508
|
+
out[i] = {**msg, "content": stub or _capped_tool_result(text, tokens, budget - spent)}
|
|
322
509
|
spent = budget
|
|
323
510
|
return out
|
|
324
511
|
|
|
@@ -337,7 +524,7 @@ def _post_compression_context(agent: Any, messages: list, system_message: Any) -
|
|
|
337
524
|
context = pruned
|
|
338
525
|
except Exception: # noqa: BLE001 - the hard cap below still bounds the context
|
|
339
526
|
log.debug("post-compression tool-result pruning failed", exc_info=True)
|
|
340
|
-
context = _cap_tool_results(context, compressor)
|
|
527
|
+
context = _cap_tool_results(context, compressor, messages)
|
|
341
528
|
estimate = None
|
|
342
529
|
try:
|
|
343
530
|
from agent.model_metadata import estimate_request_tokens_rough
|
|
@@ -350,6 +537,40 @@ def _post_compression_context(agent: Any, messages: list, system_message: Any) -
|
|
|
350
537
|
return context, estimate if isinstance(estimate, int) and estimate > 0 else None
|
|
351
538
|
|
|
352
539
|
|
|
540
|
+
# TAL-887: the Agent's password-manager prompts, answered like a clarify; the waits are the Agent's own surfaces' (its TUI).
|
|
541
|
+
_VAULT_PROMPT_TIMEOUTS = {"vault_unlock": 120, "vault_save_login": 180, "vault_code": 180, "skill_secret": 120}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _set_vault_session(session_id) -> None:
|
|
545
|
+
"""Bind the session an unlock made on this thread belongs to (released with it)."""
|
|
546
|
+
try:
|
|
547
|
+
from agent.vault_backends import unlock
|
|
548
|
+
except Exception: # noqa: BLE001 - an Agent without the credential vault
|
|
549
|
+
return
|
|
550
|
+
unlock.set_current_session_id(session_id)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _release_vault_session(session_id) -> None:
|
|
554
|
+
"""Lock the password managers ``session_id`` unlocked; other sessions keep theirs."""
|
|
555
|
+
try:
|
|
556
|
+
from agent.vault_backends.unlock import release_session
|
|
557
|
+
except Exception: # noqa: BLE001 - an Agent without the credential vault
|
|
558
|
+
return
|
|
559
|
+
release_session(session_id)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _set_vault_prompts(session_id, unlock_cb, save_login_cb, code_cb) -> None:
|
|
563
|
+
"""Bind (or with ``None``s clear) the Agent's per-thread vault prompts and the session an unlock belongs to."""
|
|
564
|
+
try:
|
|
565
|
+
from agent.vault_backends import unlock
|
|
566
|
+
except Exception: # noqa: BLE001 - an Agent without the credential vault
|
|
567
|
+
return
|
|
568
|
+
_set_vault_session(session_id)
|
|
569
|
+
unlock.set_unlock_prompt_callback(unlock_cb)
|
|
570
|
+
unlock.set_save_login_prompt_callback(save_login_cb)
|
|
571
|
+
unlock.set_code_prompt_callback(code_cb)
|
|
572
|
+
|
|
573
|
+
|
|
353
574
|
def _clarify_timeout(params: dict) -> int:
|
|
354
575
|
"""Predecessor ``_clarify_timeout_seconds``: an explicit request value wins, else the Agent's resolver over config."""
|
|
355
576
|
explicit = params.get("clarify_timeout_seconds")
|
|
@@ -481,7 +702,7 @@ def _resolve_runtime(provider: str | None, model: str) -> dict:
|
|
|
481
702
|
try:
|
|
482
703
|
runtime = resolve_runtime_provider(requested=provider or None, target_model=model or None)
|
|
483
704
|
except Exception as exc: # noqa: BLE001
|
|
484
|
-
raise
|
|
705
|
+
raise runtime_failure(exc, provider) from exc
|
|
485
706
|
if not isinstance(runtime, dict):
|
|
486
707
|
runtime = dict(vars(runtime)) if hasattr(runtime, "__dict__") else {}
|
|
487
708
|
return runtime
|
|
@@ -520,14 +741,8 @@ def _turn_identity(session_id: str, workspace: str):
|
|
|
520
741
|
except Exception: # noqa: BLE001
|
|
521
742
|
log.debug("per-turn session context bind failed", exc_info=True)
|
|
522
743
|
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
|
|
744
|
+
with session_workspace(workspace):
|
|
745
|
+
yield
|
|
531
746
|
finally:
|
|
532
747
|
for reset in reversed(resets):
|
|
533
748
|
try:
|
|
@@ -541,64 +756,309 @@ def _turn_identity(session_id: str, workspace: str):
|
|
|
541
756
|
_LEGACY_TOOLSET_ALIASES = {"hermes": ("hermes-cli", "hermes-api-server")}
|
|
542
757
|
|
|
543
758
|
|
|
544
|
-
def _turn_toolsets(toolsets) -> list[str]:
|
|
759
|
+
def _turn_toolsets(toolsets, cfg: dict | None = None) -> list[str]:
|
|
545
760
|
"""The session's override as sent, else the profile's configured toolsets. ``None`` would hand the Agent every
|
|
546
761
|
registered toolset."""
|
|
547
|
-
return toolsets if isinstance(toolsets, list) else _profile_toolsets()
|
|
762
|
+
return toolsets if isinstance(toolsets, list) else _profile_toolsets(cfg)
|
|
548
763
|
|
|
549
764
|
|
|
550
|
-
def _profile_toolsets() -> list[str]:
|
|
765
|
+
def _profile_toolsets(cfg: dict | None = None) -> list[str]:
|
|
551
766
|
"""The profile's ``platform_toolsets.cli`` through the Agent's own resolver (default-off toolsets stay off), like
|
|
552
767
|
``hermes chat``. Runs under the call's ``scoped_home``."""
|
|
553
768
|
from hermes_cli.config import load_config
|
|
554
769
|
from hermes_cli.tools_config import _get_platform_tools
|
|
555
770
|
|
|
556
|
-
resolved = sorted(_get_platform_tools(load_config() or {}, "cli"))
|
|
771
|
+
resolved = sorted(_get_platform_tools((load_config() or {}) if cfg is None else cfg, "cli"))
|
|
557
772
|
return list(dict.fromkeys(name for raw in resolved for name in _LEGACY_TOOLSET_ALIASES.get(raw, (raw,))))
|
|
558
773
|
|
|
559
774
|
|
|
560
|
-
def _discover_mcp_tools(run: _Run) -> None:
|
|
561
|
-
"""
|
|
562
|
-
|
|
563
|
-
``scoped_home``; a failure leaves the turn without MCP tools rather than failing it. A Stop does not wait for a slow
|
|
564
|
-
server: the turn moves on to its cancelled end while the connect finishes in the background."""
|
|
565
|
-
from .commands import retire_unscoped_launch_mcp_servers
|
|
775
|
+
def _discover_mcp_tools(run: _Run) -> threading.Event | None:
|
|
776
|
+
"""Wait for the profile's first discovery; later turns give a shared background retry at most two seconds."""
|
|
777
|
+
from .commands import discover_profile_mcp_tools, profile_mcp_tools_discovered
|
|
566
778
|
|
|
567
|
-
|
|
779
|
+
profile_home = run.home
|
|
780
|
+
home = os.path.realpath(profile_home)
|
|
781
|
+
deadline = time.monotonic() + _MCP_REDISCOVERY_WAIT if profile_mcp_tools_discovered(home) else None
|
|
568
782
|
|
|
569
783
|
def discover() -> None:
|
|
570
784
|
try:
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
discover_mcp_tools()
|
|
785
|
+
discovered = discover_profile_mcp_tools()
|
|
786
|
+
refresh_profile_agents_mcp_tools(profile_home)
|
|
787
|
+
done.startup_outcome = "ok" if discovered else "noop"
|
|
575
788
|
except Exception: # noqa: BLE001
|
|
789
|
+
done.startup_outcome = "error"
|
|
576
790
|
log.warning("MCP discovery failed", exc_info=True)
|
|
577
791
|
finally:
|
|
792
|
+
with _RELEASES_LOCK:
|
|
793
|
+
_RELEASES.pop(worker, None)
|
|
578
794
|
done.set()
|
|
579
795
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
796
|
+
with _MCP_DISCOVERY_LOCK:
|
|
797
|
+
if _DRAINING.is_set():
|
|
798
|
+
raise RpcError("The sidecar is shutting down.", condition="sidecar_unavailable")
|
|
799
|
+
if home in _MCP_CLOSING_HOMES:
|
|
800
|
+
raise RpcError("The profile is being deleted.", condition="profile_unavailable")
|
|
801
|
+
done = _MCP_DISCOVERIES.get(home)
|
|
802
|
+
if done is None or done.is_set():
|
|
803
|
+
done = threading.Event()
|
|
804
|
+
done.startup_outcome = "unknown"
|
|
805
|
+
_MCP_DISCOVERIES[home] = done
|
|
806
|
+
# The copied context carries the turn's home override and secret scope into the worker.
|
|
807
|
+
worker = threading.Thread(target=contextvars.copy_context().run, args=(discover,), daemon=True, name=f"chat-mcp-{run.stream_id[:8]}")
|
|
808
|
+
worker.failed = False
|
|
809
|
+
# Existing profile/shutdown custody includes this worker after its originating turn has ended.
|
|
810
|
+
with _RELEASES_LOCK:
|
|
811
|
+
_RELEASES[worker] = profile_home
|
|
812
|
+
try:
|
|
813
|
+
worker.start()
|
|
814
|
+
except Exception:
|
|
815
|
+
with _RELEASES_LOCK:
|
|
816
|
+
_RELEASES.pop(worker, None)
|
|
817
|
+
done.set()
|
|
818
|
+
raise
|
|
819
|
+
while not done.is_set():
|
|
583
820
|
if run.cancel.is_set() or run.ctx.cancelled:
|
|
821
|
+
run.mcp_discovery_outcome = "cancelled"
|
|
822
|
+
return None
|
|
823
|
+
remaining = None if deadline is None else deadline - time.monotonic()
|
|
824
|
+
if remaining is not None and remaining <= 0:
|
|
825
|
+
run.mcp_discovery_outcome = "timeout"
|
|
826
|
+
run.ctx.emit("notice", {"key": _MCP_PENDING_KEY, "level": "info", "kind": "sticky",
|
|
827
|
+
"text": "Some tools are still connecting. Using available tools for this turn.",
|
|
828
|
+
"lapses_with_agent": False})
|
|
829
|
+
return done
|
|
830
|
+
done.wait(0.25 if remaining is None else min(0.25, remaining))
|
|
831
|
+
run.mcp_discovery_outcome = getattr(done, "startup_outcome", "ok")
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
# Serializes every hook change the sidecar makes, so two turns or a turn and a profile deletion never interleave a
|
|
836
|
+
# drop with a registration.
|
|
837
|
+
_HOOKS_LOCK = threading.Lock()
|
|
838
|
+
_HOOK_CALLBACK_PREFIXES = {"agent.shell_hooks": "shell_hook[", "agent.outbound_webhooks": "outbound_webhook["}
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _drop_home_callbacks(*modules) -> None:
|
|
842
|
+
"""Forget this home's registrations in each Agent hook module and remove their callbacks from the home's plugin
|
|
843
|
+
manager; the Agent has no teardown for either registry. Runs under the call's ``scoped_home`` and ``_HOOKS_LOCK``."""
|
|
844
|
+
from agent import shell_hooks
|
|
845
|
+
from hermes_cli.plugins import get_plugin_manager
|
|
846
|
+
|
|
847
|
+
prefixes = tuple(_HOOK_CALLBACK_PREFIXES[module.__name__] for module in modules)
|
|
848
|
+
for module in modules:
|
|
849
|
+
shell_hooks._forget_home_registrations(module._registered, module._registered_lock)
|
|
850
|
+
manager = get_plugin_manager()
|
|
851
|
+
with shell_hooks._registered_lock:
|
|
852
|
+
for event, callbacks in list(manager._hooks.items()):
|
|
853
|
+
# Replaced, never edited in place: a concurrent invoke_hook keeps iterating the old list.
|
|
854
|
+
manager._hooks[event] = [cb for cb in callbacks if not getattr(cb, "__name__", "").startswith(prefixes)]
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _hook_state_trusted(shell_hooks, cfg: dict) -> bool:
|
|
858
|
+
"""``sync_from_config``'s removal guard for Agents without it: an empty (failed-open) config, an unreadable
|
|
859
|
+
allowlist or a ``hooks:`` entry the Agent could not parse must never disarm a registered guard."""
|
|
860
|
+
if not cfg:
|
|
861
|
+
return False
|
|
862
|
+
try:
|
|
863
|
+
allowlist = json.loads(shell_hooks.allowlist_path().read_text(encoding="utf-8-sig"))
|
|
864
|
+
except FileNotFoundError:
|
|
865
|
+
allowlist = {"approvals": []}
|
|
866
|
+
except (OSError, ValueError):
|
|
867
|
+
return False
|
|
868
|
+
if not isinstance(allowlist, dict) or not isinstance(allowlist.get("approvals"), list):
|
|
869
|
+
return False
|
|
870
|
+
hooks = cfg.get("hooks")
|
|
871
|
+
if hooks is None:
|
|
872
|
+
return True
|
|
873
|
+
if not isinstance(hooks, dict):
|
|
874
|
+
return False
|
|
875
|
+
events = [entries for event, entries in hooks.items() if event not in ("output_spill", "outbound") and entries is not None]
|
|
876
|
+
if not all(isinstance(entries, list) for entries in events):
|
|
877
|
+
return False
|
|
878
|
+
return len(shell_hooks.iter_configured_hooks(cfg)) == sum(len(entries) for entries in events)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _replace_shell_hooks(shell_hooks, cfg: dict) -> None:
|
|
882
|
+
"""Agents without ``sync_from_config``: wire the home's consented hooks from ``cfg`` and drop the rest, so a hook
|
|
883
|
+
removed from config or revoked stops, as it does on v0.22.3. The stale callbacks go only after the current set is
|
|
884
|
+
wired, so a concurrent turn's tool call always meets its guard. Add-only when the hook state is not trusted."""
|
|
885
|
+
if not _hook_state_trusted(shell_hooks, cfg):
|
|
886
|
+
shell_hooks.register_from_config(cfg, accept_hooks=False)
|
|
887
|
+
return
|
|
888
|
+
from hermes_cli.plugins import get_plugin_manager
|
|
889
|
+
|
|
890
|
+
manager = get_plugin_manager()
|
|
891
|
+
stale = [cb for callbacks in manager._hooks.values() for cb in callbacks if getattr(cb, "__name__", "").startswith("shell_hook[")]
|
|
892
|
+
shell_hooks._forget_home_registrations(shell_hooks._registered, shell_hooks._registered_lock)
|
|
893
|
+
shell_hooks.register_from_config(cfg, accept_hooks=False)
|
|
894
|
+
with shell_hooks._registered_lock:
|
|
895
|
+
for event, callbacks in list(manager._hooks.items()):
|
|
896
|
+
manager._hooks[event] = [cb for cb in callbacks if not any(cb is old for old in stale)]
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _register_profile_hooks(home: Path) -> None:
|
|
900
|
+
"""Wire the profile's consented shell hooks and current outbound webhooks before the turn's tools run, like the
|
|
901
|
+
gateway on every session open (TAL-1258). Hooks added or approved since the last call join this one; hooks removed
|
|
902
|
+
from config or revoked stop, through ``sync_from_config`` or its sidecar twin for older Agents. Outbound webhooks
|
|
903
|
+
are add-only on every Agent, so each call replaces the home's set with the configured one (a concurrent turn's
|
|
904
|
+
notification can miss that instant; webhooks only observe). Consent comes from the allowlist or an opt-in in
|
|
905
|
+
force: stdin is the RPC pipe, so nothing prompts. A failure is logged and never blocks the turn."""
|
|
906
|
+
try:
|
|
907
|
+
from agent import outbound_webhooks, shell_hooks
|
|
908
|
+
from hermes_cli.config import load_config
|
|
909
|
+
|
|
910
|
+
with _HOOKS_LOCK:
|
|
911
|
+
sync = getattr(shell_hooks, "sync_from_config", None)
|
|
912
|
+
if sync is not None:
|
|
913
|
+
sync(load_config)
|
|
914
|
+
else:
|
|
915
|
+
_replace_shell_hooks(shell_hooks, load_config())
|
|
916
|
+
_drop_home_callbacks(outbound_webhooks)
|
|
917
|
+
outbound_webhooks.register_from_config(load_config())
|
|
918
|
+
except Exception: # noqa: BLE001
|
|
919
|
+
log.warning("shell-hook sync failed; keeping the registered hooks", exc_info=True)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def forget_profile_hooks(home: Path) -> None:
|
|
923
|
+
"""Profile deletion: drop the home's shell hooks and outbound webhooks. The Agent keys both registries and its
|
|
924
|
+
plugin manager by path, so a profile recreated at that path would otherwise fire hooks its new config and allowlist
|
|
925
|
+
never approved. Raises rather than let the profile go with its hooks still wired."""
|
|
926
|
+
try:
|
|
927
|
+
from agent import outbound_webhooks, shell_hooks
|
|
928
|
+
except ImportError: # no Agent, so no hook was ever registered
|
|
929
|
+
return
|
|
930
|
+
with _HOOKS_LOCK, scoped_home(home):
|
|
931
|
+
_drop_home_callbacks(shell_hooks, outbound_webhooks)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@contextlib.contextmanager
|
|
935
|
+
def agent_scope(home: Path):
|
|
936
|
+
"""``scoped_home`` for a call that runs Agent work: the profile's hooks are wired before any of it (TAL-1258), so
|
|
937
|
+
the call's first LLM request, tool call or lifecycle event already sees them, even as a session's first work after
|
|
938
|
+
a sidecar restart."""
|
|
939
|
+
with scoped_home(home):
|
|
940
|
+
_register_profile_hooks(home)
|
|
941
|
+
yield
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class _ArchiveCallback:
|
|
945
|
+
"""Queued work shares the release threads' join/liveness contract without owning an OS thread."""
|
|
946
|
+
name = "session-archive"
|
|
947
|
+
failed = False
|
|
948
|
+
|
|
949
|
+
def __init__(self, callback: Callable[[], None]):
|
|
950
|
+
self.callback = callback
|
|
951
|
+
self.done = threading.Event()
|
|
952
|
+
|
|
953
|
+
def run(self) -> None:
|
|
954
|
+
try:
|
|
955
|
+
self.callback()
|
|
956
|
+
finally:
|
|
957
|
+
self.done.set()
|
|
958
|
+
|
|
959
|
+
def join(self, timeout=None) -> None:
|
|
960
|
+
self.done.wait(timeout)
|
|
961
|
+
|
|
962
|
+
def is_alive(self) -> bool:
|
|
963
|
+
return not self.done.is_set()
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _archive_worker(pending) -> None:
|
|
967
|
+
while True:
|
|
968
|
+
work = pending.get()
|
|
969
|
+
try:
|
|
970
|
+
if work is None:
|
|
971
|
+
return
|
|
972
|
+
try:
|
|
973
|
+
work.run()
|
|
974
|
+
except BaseException: # a plugin must not kill a shared worker
|
|
975
|
+
log.warning("Session archive notification worker failed")
|
|
976
|
+
finally:
|
|
977
|
+
pending.task_done()
|
|
978
|
+
del work
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _schedule_archive(work: _ArchiveCallback, home: Path) -> None:
|
|
982
|
+
# Admission shares the profile-deletion/shutdown fence; queued work uses the existing release custody roster.
|
|
983
|
+
with _MCP_DISCOVERY_LOCK:
|
|
984
|
+
if _DRAINING.is_set() or os.path.realpath(home) in _MCP_CLOSING_HOMES:
|
|
985
|
+
log.warning("Session archive notification unavailable during profile or sidecar shutdown")
|
|
584
986
|
return
|
|
987
|
+
if len(_ARCHIVE_WORKERS) < _ARCHIVE_MAX_WORKERS:
|
|
988
|
+
thread = threading.Thread(target=_archive_worker, args=(_ARCHIVE_QUEUE,), name="session-archive-worker", daemon=True)
|
|
989
|
+
try:
|
|
990
|
+
thread.start()
|
|
991
|
+
except RuntimeError:
|
|
992
|
+
log.warning("Session archive notification worker could not start")
|
|
993
|
+
if not _ARCHIVE_WORKERS:
|
|
994
|
+
return
|
|
995
|
+
else:
|
|
996
|
+
_ARCHIVE_WORKERS.append(thread)
|
|
997
|
+
with _RELEASES_LOCK:
|
|
998
|
+
_RELEASES[work] = home
|
|
999
|
+
try:
|
|
1000
|
+
_ARCHIVE_QUEUE.put_nowait(work)
|
|
1001
|
+
except queue.Full:
|
|
1002
|
+
_RELEASES.pop(work, None)
|
|
1003
|
+
log.warning("Session archive notification queue is full")
|
|
585
1004
|
|
|
586
1005
|
|
|
587
|
-
def
|
|
1006
|
+
def _notify_archive(home: Path, session_id: str, lineage: list[str] | None) -> None:
|
|
1007
|
+
import hermes_state_common
|
|
1008
|
+
|
|
1009
|
+
emit = getattr(hermes_state_common, "emit_session_deleted", None)
|
|
1010
|
+
if emit is None:
|
|
1011
|
+
return
|
|
1012
|
+
# Upstream hook warnings can include arbitrary callback exceptions. Keep one payload-free diagnostic.
|
|
1013
|
+
thread_id, warned = threading.get_ident(), False
|
|
1014
|
+
|
|
1015
|
+
def bounded_diagnostic(record):
|
|
1016
|
+
nonlocal warned
|
|
1017
|
+
if record.thread != thread_id:
|
|
1018
|
+
return True
|
|
1019
|
+
warned = warned or record.levelno >= logging.WARNING
|
|
1020
|
+
return False
|
|
1021
|
+
|
|
1022
|
+
handlers = list(logging.getLogger().handlers)
|
|
1023
|
+
for handler in handlers:
|
|
1024
|
+
handler.addFilter(bounded_diagnostic)
|
|
1025
|
+
try:
|
|
1026
|
+
# The emitter only needs db_path for profile identity. Opening SessionDB would migrate an owner's store.
|
|
1027
|
+
emit(SimpleNamespace(db_path=home / "state.db"), [session_id, *(lineage or [])],
|
|
1028
|
+
reason="archive", primary=session_id, sessions_dir=home / "sessions")
|
|
1029
|
+
finally:
|
|
1030
|
+
for handler in handlers:
|
|
1031
|
+
handler.removeFilter(bounded_diagnostic)
|
|
1032
|
+
if warned:
|
|
1033
|
+
log.warning("Session archive notification reported a callback failure")
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _profile_fallback_chain(cfg: dict | None = None) -> list[dict] | None:
|
|
588
1037
|
"""The profile's fallback routes (``fallback_providers`` first, then legacy ``fallback_model``, deduplicated)
|
|
589
1038
|
through the Agent's own resolver, like the CLI and gateway. Runs under the call's ``scoped_home``."""
|
|
590
1039
|
from hermes_cli.config import load_config
|
|
591
1040
|
from hermes_cli.fallback_config import get_fallback_chain
|
|
592
1041
|
|
|
593
|
-
return get_fallback_chain(load_config() or {}) or None
|
|
1042
|
+
return copy.deepcopy(get_fallback_chain((load_config() or {}) if cfg is None else cfg)) or None
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def _profile_prefill_messages(cfg: dict | None = None) -> list[dict] | None:
|
|
1046
|
+
"""The profile's few-shot prefill (``HERMES_PREFILL_MESSAGES_FILE``, then ``prefill_messages_file``, then legacy
|
|
1047
|
+
``agent.prefill_messages_file``; relative to the profile) through the Agent's own loader, like the CLI, gateway and
|
|
1048
|
+
Desktop. A missing or empty file is None. Runs under the call's ``scoped_home``."""
|
|
1049
|
+
from hermes_cli.cli_config_load import _load_prefill_messages, _resolve_prefill_messages_file
|
|
1050
|
+
from hermes_cli.config import load_config
|
|
1051
|
+
from hermes_constants import get_hermes_home
|
|
1052
|
+
|
|
1053
|
+
return _load_prefill_messages(_resolve_prefill_messages_file((load_config() or {}) if cfg is None else cfg), get_hermes_home()) or None
|
|
594
1054
|
|
|
595
1055
|
|
|
596
|
-
def _main_model_request_overrides(model: str, provider) -> dict | None:
|
|
1056
|
+
def _main_model_request_overrides(model: str, provider, base_url: str = "", cfg: dict | None = None) -> dict | None:
|
|
597
1057
|
"""Predecessor ``_main_model_request_overrides``: the profile's ``model.service_tier`` and ``model.extra_body``
|
|
598
1058
|
(written by ``/api/model/set``) apply only to turns on that configured main model. Runs under ``scoped_home``."""
|
|
599
1059
|
from hermes_cli.config import load_config
|
|
600
1060
|
|
|
601
|
-
model_cfg = (load_config() or {}).get("model")
|
|
1061
|
+
model_cfg = ((load_config() or {}) if cfg is None else cfg).get("model")
|
|
602
1062
|
if not isinstance(model_cfg, dict):
|
|
603
1063
|
return None
|
|
604
1064
|
main_model = str(model_cfg.get("default") or model_cfg.get("name") or "").strip()
|
|
@@ -606,17 +1066,56 @@ def _main_model_request_overrides(model: str, provider) -> dict | None:
|
|
|
606
1066
|
if not main_model or main_model != model.strip():
|
|
607
1067
|
return None
|
|
608
1068
|
# An unset or ``auto`` main provider resolves at runtime, so the model id alone identifies the main model.
|
|
609
|
-
|
|
1069
|
+
from hermes_cli.models import normalize_provider
|
|
1070
|
+
|
|
1071
|
+
if main_provider not in ("", "auto") and normalize_provider(main_provider) != normalize_provider(str(provider or "").strip().lower()):
|
|
610
1072
|
return None
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1073
|
+
from ..speed import fast_overrides
|
|
1074
|
+
|
|
1075
|
+
overrides = fast_overrides(model, str(provider or ""), base_url, str(model_cfg.get("service_tier") or "").strip().lower()) or {}
|
|
614
1076
|
extra_body = model_cfg.get("extra_body")
|
|
615
1077
|
if isinstance(extra_body, dict) and extra_body:
|
|
616
|
-
overrides["extra_body"] = extra_body
|
|
1078
|
+
overrides["extra_body"] = copy.deepcopy(extra_body)
|
|
617
1079
|
return overrides or None
|
|
618
1080
|
|
|
619
1081
|
|
|
1082
|
+
def _request_overrides(model: str, provider, runtime: dict, cfg: dict | None = None) -> dict | None:
|
|
1083
|
+
"""The resolver's ``request_overrides`` (a custom provider entry's ``extra_body``) under the main-model ones, whose
|
|
1084
|
+
``extra_body`` wins key by key, the Agent's own merge order. Runs under ``scoped_home``."""
|
|
1085
|
+
merged = dict(runtime.get("request_overrides") or {})
|
|
1086
|
+
main = _main_model_request_overrides(model, provider, str(runtime.get("base_url") or ""), cfg) or {}
|
|
1087
|
+
extra_body = {**(merged.get("extra_body") or {}), **(main.get("extra_body") or {})}
|
|
1088
|
+
merged.update(main)
|
|
1089
|
+
if extra_body:
|
|
1090
|
+
merged["extra_body"] = extra_body
|
|
1091
|
+
return merged or None
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _provider_routing_kwargs(cfg: dict | None = None) -> dict:
|
|
1095
|
+
"""The profile's OpenRouter ``provider_routing`` and ``openrouter.min_coding_score`` as ``AIAgent`` kwargs, with the
|
|
1096
|
+
Agent's own mapping (``gateway/session_worker_construct.routing_agent_kwargs``). The score is coerced like the CLI's
|
|
1097
|
+
(``cli_init_mixin``), so a quoted number counts and a value outside 0..1 stays unset. Runs under ``scoped_home``."""
|
|
1098
|
+
from hermes_cli.config import load_config
|
|
1099
|
+
|
|
1100
|
+
if cfg is None:
|
|
1101
|
+
cfg = load_config() or {}
|
|
1102
|
+
routing = cfg.get("provider_routing")
|
|
1103
|
+
routing = routing if isinstance(routing, dict) else {}
|
|
1104
|
+
openrouter = cfg.get("openrouter")
|
|
1105
|
+
raw_score = openrouter.get("min_coding_score") if isinstance(openrouter, dict) else None
|
|
1106
|
+
try:
|
|
1107
|
+
score = float(raw_score) if raw_score not in (None, "") else None
|
|
1108
|
+
except (TypeError, ValueError):
|
|
1109
|
+
score = None
|
|
1110
|
+
return copy.deepcopy({
|
|
1111
|
+
"providers_allowed": routing.get("only"), "providers_ignored": routing.get("ignore"),
|
|
1112
|
+
"providers_order": routing.get("order"), "provider_sort": routing.get("sort"),
|
|
1113
|
+
"provider_require_parameters": routing.get("require_parameters", False),
|
|
1114
|
+
"provider_data_collection": routing.get("data_collection"),
|
|
1115
|
+
"openrouter_min_coding_score": score if score is not None and 0.0 <= score <= 1.0 else None,
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
|
|
620
1119
|
def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, kwargs: dict) -> str:
|
|
621
1120
|
"""Cache identity of an ``AIAgent``: everything its constructor bound from the resolved runtime, so a rotated key,
|
|
622
1121
|
a different API mode, ACP command, or credential pool never reuses an agent built for the old bundle. The key
|
|
@@ -628,18 +1127,95 @@ def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, k
|
|
|
628
1127
|
bundle = {
|
|
629
1128
|
"model": model, "provider": provider, "base_url": runtime.get("base_url"), "api_key": key_digest,
|
|
630
1129
|
"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,
|
|
1130
|
+
"credential_pool": pool_identity, "toolsets": toolsets, "home": home, "cwd": kwargs.get("cwd"),
|
|
1131
|
+
"disabled_toolsets": kwargs.get("disabled_toolsets"),
|
|
632
1132
|
"max_iterations": kwargs.get("max_iterations"), "max_tokens": kwargs.get("max_tokens"),
|
|
633
1133
|
# Bound at construction too: a reasoning-effort change from the composer must build a fresh agent.
|
|
634
1134
|
"reasoning_config": kwargs.get("reasoning_config"),
|
|
635
1135
|
"request_overrides": kwargs.get("request_overrides"),
|
|
1136
|
+
"provider_routing": [kwargs.get(name) for name in (
|
|
1137
|
+
"providers_allowed", "providers_ignored", "providers_order", "provider_sort", "provider_require_parameters",
|
|
1138
|
+
"provider_data_collection", "openrouter_min_coding_score")],
|
|
636
1139
|
"checkpoints": [kwargs.get(name) for name in ("checkpoints_enabled", "checkpoint_max_snapshots", "checkpoint_max_total_size_mb", "checkpoint_max_file_size_mb")],
|
|
637
1140
|
# Entries may carry their own ``api_key``.
|
|
638
1141
|
"fallback_model": hashlib.sha256(json.dumps(kwargs.get("fallback_model"), sort_keys=True, default=str).encode("utf-8")).hexdigest()[:16],
|
|
1142
|
+
# Read each turn, so an edited prefill file builds a fresh agent.
|
|
1143
|
+
"prefill_messages": hashlib.sha256(json.dumps(kwargs.get("prefill_messages"), sort_keys=True, default=str).encode("utf-8")).hexdigest()[:16],
|
|
639
1144
|
}
|
|
640
1145
|
return json.dumps(bundle, sort_keys=True, default=str)
|
|
641
1146
|
|
|
642
1147
|
|
|
1148
|
+
def _agent_kwargs(AIAgent, params: dict, session_id: str, model: str, provider, runtime: dict, toolsets, cfg: dict | None = None) -> dict:
|
|
1149
|
+
"""The ``AIAgent`` arguments of a session's agent apart from a turn's callbacks: the resolved runtime, the session's
|
|
1150
|
+
toolsets and budgets, and the profile's fallback chain, request overrides, prefill and checkpoints. Everything
|
|
1151
|
+
``_agent_signature`` reads is here, so an agent ``/refine`` builds (TAL-902) is the one the next turn reuses. Runs under
|
|
1152
|
+
the call's ``scoped_home``."""
|
|
1153
|
+
kwargs: dict = dict(
|
|
1154
|
+
model=model,
|
|
1155
|
+
provider=provider,
|
|
1156
|
+
base_url=runtime.get("base_url"),
|
|
1157
|
+
api_key=runtime.get("api_key"),
|
|
1158
|
+
platform="webui",
|
|
1159
|
+
quiet_mode=True,
|
|
1160
|
+
enabled_toolsets=toolsets,
|
|
1161
|
+
disabled_toolsets=disabled_toolsets(cfg),
|
|
1162
|
+
session_id=session_id,
|
|
1163
|
+
)
|
|
1164
|
+
for name, value in (
|
|
1165
|
+
("api_mode", runtime.get("api_mode")),
|
|
1166
|
+
("acp_command", runtime.get("acp_command")),
|
|
1167
|
+
("acp_args", runtime.get("acp_args")),
|
|
1168
|
+
("credential_pool", runtime.get("credential_pool")),
|
|
1169
|
+
("gateway_session_key", session_id),
|
|
1170
|
+
# TAL-903: anchors subdirectory AGENTS.md discovery (and the memory provider) at the session workspace, when the
|
|
1171
|
+
# server says it is a host path; a remote terminal backend's target-side path must never be read on the host.
|
|
1172
|
+
("cwd", params.get("workspace") if params.get("workspace_local") is True else None),
|
|
1173
|
+
):
|
|
1174
|
+
if _supported(AIAgent, name) and value is not None:
|
|
1175
|
+
kwargs[name] = value
|
|
1176
|
+
if isinstance(params.get("max_iterations"), int) and params["max_iterations"] > 0 and _supported(AIAgent, "max_iterations"):
|
|
1177
|
+
kwargs["max_iterations"] = params["max_iterations"]
|
|
1178
|
+
if isinstance(params.get("max_tokens"), int) and params["max_tokens"] > 0 and _supported(AIAgent, "max_tokens"):
|
|
1179
|
+
kwargs["max_tokens"] = params["max_tokens"]
|
|
1180
|
+
# Predecessor: `agent.reasoning_effort` (already coerced for the model) → AIAgent ``reasoning_config``.
|
|
1181
|
+
reasoning_config = params.get("reasoning_config")
|
|
1182
|
+
if isinstance(reasoning_config, dict) and _supported(AIAgent, "reasoning_config"):
|
|
1183
|
+
kwargs["reasoning_config"] = reasoning_config
|
|
1184
|
+
fallback_chain = _profile_fallback_chain(cfg)
|
|
1185
|
+
if fallback_chain and _supported(AIAgent, "fallback_model"):
|
|
1186
|
+
kwargs["fallback_model"] = fallback_chain
|
|
1187
|
+
request_overrides = _request_overrides(model, provider, runtime, cfg)
|
|
1188
|
+
if request_overrides and _supported(AIAgent, "request_overrides"):
|
|
1189
|
+
kwargs["request_overrides"] = request_overrides
|
|
1190
|
+
prefill_messages = _profile_prefill_messages(cfg)
|
|
1191
|
+
if prefill_messages and _supported(AIAgent, "prefill_messages"):
|
|
1192
|
+
kwargs["prefill_messages"] = prefill_messages
|
|
1193
|
+
# TAL-976: the profile's OpenRouter provider routing, as the Agent's own CLI and gateway turns read it.
|
|
1194
|
+
for name, value in _provider_routing_kwargs(cfg).items():
|
|
1195
|
+
if _supported(AIAgent, name):
|
|
1196
|
+
kwargs[name] = value
|
|
1197
|
+
# TAL-913: the profile's ``checkpoints`` section, as the Agent's own CLI and gateway turns read it.
|
|
1198
|
+
for name, value in checkpoint_kwargs(cfg).items():
|
|
1199
|
+
if _supported(AIAgent, name):
|
|
1200
|
+
kwargs[name] = value
|
|
1201
|
+
return kwargs
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _build_agent(AIAgent, kwargs: dict, home, session_id: str, state_db: bool = True):
|
|
1205
|
+
"""A new session agent on the profile's ``state.db`` (unless ``state_db`` is false), which it releases with it."""
|
|
1206
|
+
session_db = _acquire_session_db(home) if state_db and _supported(AIAgent, "session_db") else None
|
|
1207
|
+
if session_db is not None:
|
|
1208
|
+
kwargs["session_db"] = session_db
|
|
1209
|
+
kwargs["session_id"] = _execution_session_id(session_db, session_id)
|
|
1210
|
+
try:
|
|
1211
|
+
agent = AIAgent(**kwargs)
|
|
1212
|
+
except BaseException:
|
|
1213
|
+
release_session_db(session_db)
|
|
1214
|
+
raise
|
|
1215
|
+
agent._talaria_session_db = session_db # released with the agent (``_release_agent``)
|
|
1216
|
+
return agent
|
|
1217
|
+
|
|
1218
|
+
|
|
643
1219
|
def _evict_idle_agents_locked() -> None:
|
|
644
1220
|
"""Trim the agent LRU (caller holds ``_AGENT_CACHE_LOCK``), never dropping a session whose turn is still running."""
|
|
645
1221
|
if len(_AGENT_CACHE) <= _AGENT_CACHE_MAX:
|
|
@@ -686,7 +1262,7 @@ def _release_agent(agent, home) -> threading.Thread:
|
|
|
686
1262
|
if not released:
|
|
687
1263
|
# The memory flush needs the profile; the LLM clients and the Codex child close regardless.
|
|
688
1264
|
call("release_clients")
|
|
689
|
-
|
|
1265
|
+
release_session_db(getattr(agent, "_talaria_session_db", None))
|
|
690
1266
|
with _RELEASES_LOCK:
|
|
691
1267
|
_RELEASES.pop(thread, None)
|
|
692
1268
|
|
|
@@ -702,7 +1278,7 @@ def _release_agents_locked(entries) -> list[threading.Thread]:
|
|
|
702
1278
|
"""Release cache entries that just left the cache (caller holds ``_AGENT_CACHE_LOCK``). An agent a turn still holds
|
|
703
1279
|
keeps its memory provider and clients; that turn releases it when it ends."""
|
|
704
1280
|
with _RUNS_LOCK:
|
|
705
|
-
held = [run.agent for run in _RUNS.values()] + _COMMITTING
|
|
1281
|
+
held = [run.agent for run in _RUNS.values()] + [agent for agent, _home in _COMMITTING]
|
|
706
1282
|
return [_release_agent(agent, home) for agent, _signature, home in entries if not any(agent is other for other in held)]
|
|
707
1283
|
|
|
708
1284
|
|
|
@@ -718,6 +1294,73 @@ def _join(threads, deadline: float) -> None:
|
|
|
718
1294
|
thread.join(max(0.0, deadline - time.monotonic()))
|
|
719
1295
|
|
|
720
1296
|
|
|
1297
|
+
def _cancel_background_reviews(entries, message: str) -> list[threading.Thread]:
|
|
1298
|
+
"""TAL-897: stop the post-turn memory/skill review fork of each ``(agent, home)``, which no turn interrupt or release
|
|
1299
|
+
reaches, so a deleted session or a stopping sidecar no longer pays for it. Each cancel then holds the agent's review
|
|
1300
|
+
slot (a run token from the Agent's ``prepare_background_review_run``, never finished), so the Agent admits no later
|
|
1301
|
+
review: not from a turn still running (a clear does not stop it), a spawn already under way, or a review deferred to
|
|
1302
|
+
or requeued on the idle queue (managed local runtime). ``skip_background_review`` spares the turn finalizer queueing
|
|
1303
|
+
one at all. Only a turn's start waits on a held slot, and an ended agent runs no further turn.
|
|
1304
|
+
|
|
1305
|
+
A cancel lasts until its fork acknowledges, which the Agent's own wait does not bound, and runs as a release under
|
|
1306
|
+
``home`` (``_RELEASES``): shutdown waits for it, and profile deletion, retries included, refuses while it runs or
|
|
1307
|
+
once it failed."""
|
|
1308
|
+
unique = {id(agent): (agent, home) for agent, home in entries if agent is not None}
|
|
1309
|
+
for agent, _home in unique.values():
|
|
1310
|
+
try:
|
|
1311
|
+
agent.skip_background_review = True
|
|
1312
|
+
except AttributeError: # an object without instance attributes forks no reviews
|
|
1313
|
+
pass
|
|
1314
|
+
try:
|
|
1315
|
+
from agent.background_review import cancel_background_review_for_live_turn, prepare_background_review_run
|
|
1316
|
+
except ImportError:
|
|
1317
|
+
return []
|
|
1318
|
+
|
|
1319
|
+
def cancel(agent) -> None:
|
|
1320
|
+
thread = threading.current_thread()
|
|
1321
|
+
try:
|
|
1322
|
+
try:
|
|
1323
|
+
# One cancel per agent at a time, so none mistakes another's freshly held slot for a review to cancel.
|
|
1324
|
+
lock = vars(agent).setdefault("_talaria_review_cancel_lock", threading.Lock())
|
|
1325
|
+
except TypeError:
|
|
1326
|
+
return # no instance attributes, so no review slot either
|
|
1327
|
+
with lock:
|
|
1328
|
+
# A spawn that slipped in between the cancel and the slot claim is cancelled on the next pass.
|
|
1329
|
+
for _ in range(3):
|
|
1330
|
+
if getattr(agent, "_talaria_review_slot", None) is not None:
|
|
1331
|
+
return
|
|
1332
|
+
cancel_background_review_for_live_turn(agent, message=message, tool_reason="session ended")
|
|
1333
|
+
slot = prepare_background_review_run(agent)
|
|
1334
|
+
if slot is not None:
|
|
1335
|
+
agent._talaria_review_slot = slot
|
|
1336
|
+
return
|
|
1337
|
+
current = getattr(agent, "_background_review_run", None)
|
|
1338
|
+
if current is None:
|
|
1339
|
+
return # not an Agent with a review slot
|
|
1340
|
+
if current.cancel_requested.is_set():
|
|
1341
|
+
# A cancelled fork past the Agent's wait may still write: wait for it to acknowledge, then claim
|
|
1342
|
+
# the slot before the Agent can requeue it.
|
|
1343
|
+
current.request_done.wait()
|
|
1344
|
+
log.warning("a background review kept restarting after its session ended")
|
|
1345
|
+
thread.failed = True
|
|
1346
|
+
except Exception: # noqa: BLE001 - teardown continues without it
|
|
1347
|
+
log.warning("cancelling a background review failed", exc_info=True)
|
|
1348
|
+
thread.failed = True
|
|
1349
|
+
finally:
|
|
1350
|
+
with _RELEASES_LOCK:
|
|
1351
|
+
_RELEASES.pop(thread, None)
|
|
1352
|
+
|
|
1353
|
+
threads = []
|
|
1354
|
+
for agent, home in unique.values():
|
|
1355
|
+
thread = threading.Thread(target=cancel, args=(agent,), daemon=True, name="chat-review-cancel")
|
|
1356
|
+
thread.failed = False # type: ignore[attr-defined] - read by profile deletion
|
|
1357
|
+
with _RELEASES_LOCK:
|
|
1358
|
+
_RELEASES[thread] = home
|
|
1359
|
+
thread.start()
|
|
1360
|
+
threads.append(thread)
|
|
1361
|
+
return threads
|
|
1362
|
+
|
|
1363
|
+
|
|
721
1364
|
def evict_all_agents() -> int:
|
|
722
1365
|
"""Drop every cached agent (credentials or environment changed underneath them)."""
|
|
723
1366
|
with _AGENT_CACHE_LOCK:
|
|
@@ -731,10 +1374,15 @@ def drain_agents(timeout: float = _AGENT_RELEASE_TIMEOUT) -> None:
|
|
|
731
1374
|
"""Shutdown: release every cached agent, stop running turns (like a Stop) so each releases the agent it holds, and
|
|
732
1375
|
wait, bounded, for those turns and every release still running."""
|
|
733
1376
|
deadline = time.monotonic() + timeout
|
|
734
|
-
|
|
735
|
-
|
|
1377
|
+
with _MCP_DISCOVERY_LOCK, _AGENT_CACHE_LOCK, _RUNS_LOCK:
|
|
1378
|
+
# Admission closes with the snapshot, so no later turn forks a review outside it.
|
|
736
1379
|
_DRAINING.set()
|
|
737
1380
|
runs = list(_RUNS.values())
|
|
1381
|
+
owners = [(entry[0], entry[2]) for entry in _AGENT_CACHE.values()] + [(run.agent, run.home) for run in runs]
|
|
1382
|
+
for run in runs:
|
|
1383
|
+
run.session_ended = True
|
|
1384
|
+
_cancel_background_reviews(owners, "sidecar shutting down")
|
|
1385
|
+
evict_all_agents()
|
|
738
1386
|
for run in runs:
|
|
739
1387
|
run.cancel.set()
|
|
740
1388
|
while time.monotonic() < deadline:
|
|
@@ -747,22 +1395,83 @@ def drain_agents(timeout: float = _AGENT_RELEASE_TIMEOUT) -> None:
|
|
|
747
1395
|
with _RELEASES_LOCK:
|
|
748
1396
|
threads = list(_RELEASES)
|
|
749
1397
|
_join(threads, deadline)
|
|
1398
|
+
# TAL-1191: the state.db handles state_db writes keep between turns; each final release checkpoints once.
|
|
1399
|
+
release_all()
|
|
1400
|
+
|
|
750
1401
|
|
|
1402
|
+
def refresh_profile_agents_mcp_tools(home) -> None:
|
|
1403
|
+
"""MCP reload: republish the live registry's tools to the profile's cached agents, like the gateway's
|
|
1404
|
+
``_mcp_reload_refresh_cached_agents``. ``content_aware`` publishes a same-name schema change on every supported
|
|
1405
|
+
Agent (0.22.0 implies it for an explicit reload, 0.21.x does not). Each agent keeps its build-time toolset
|
|
1406
|
+
selection. An agent whose refresh fails leaves the cache, so its session's next turn builds one from the live
|
|
1407
|
+
registry instead of keeping stale schemas; the rest still refresh."""
|
|
1408
|
+
from tools.mcp_tool_agent import refresh_agent_mcp_tools
|
|
751
1409
|
|
|
752
|
-
def release_profile_agents(home) -> None:
|
|
753
|
-
"""Profile deletion: release the profile's cached agents and wait for every release running under its home. Fails
|
|
754
|
-
rather than let the home go while a memory flush still runs under it."""
|
|
755
1410
|
target = os.path.realpath(home)
|
|
756
1411
|
with _AGENT_CACHE_LOCK:
|
|
757
|
-
entries = [
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1412
|
+
entries = [(sid, entry) for sid, entry in _AGENT_CACHE.items() if os.path.realpath(entry[2]) == target]
|
|
1413
|
+
_COMMITTING.extend((entry[0], entry[2]) for _sid, entry in entries)
|
|
1414
|
+
try:
|
|
1415
|
+
for session_id, (agent, _signature, _home) in entries:
|
|
1416
|
+
try:
|
|
1417
|
+
refresh_agent_mcp_tools(agent, quiet_mode=True, content_aware=True)
|
|
1418
|
+
except Exception: # noqa: BLE001
|
|
1419
|
+
log.warning("Failed to refresh MCP tools for session %s", session_id, exc_info=True)
|
|
1420
|
+
with _AGENT_CACHE_LOCK:
|
|
1421
|
+
cached = _AGENT_CACHE.get(session_id)
|
|
1422
|
+
# A turn may have replaced or evicted this agent while its refresh ran.
|
|
1423
|
+
if cached is not None and cached[0] is agent:
|
|
1424
|
+
_AGENT_CACHE.pop(session_id)
|
|
1425
|
+
finally:
|
|
1426
|
+
with _AGENT_CACHE_LOCK:
|
|
1427
|
+
for _sid, (agent, _signature, owner) in entries:
|
|
1428
|
+
_COMMITTING.remove((agent, owner))
|
|
1429
|
+
_release_if_dropped_locked(agent, owner)
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
@contextlib.contextmanager
|
|
1433
|
+
def release_profile_agents(home):
|
|
1434
|
+
"""Fence discovery and drain this profile's agents and background work before the caller removes its home."""
|
|
1435
|
+
from .commands import forget_profile_mcp_tools
|
|
1436
|
+
|
|
1437
|
+
target = os.path.realpath(home)
|
|
1438
|
+
with _MCP_DISCOVERY_LOCK:
|
|
1439
|
+
if target in _MCP_CLOSING_HOMES:
|
|
1440
|
+
raise RpcError("The profile is already being deleted.", condition="profile_unavailable")
|
|
1441
|
+
_MCP_CLOSING_HOMES.add(target)
|
|
1442
|
+
try:
|
|
1443
|
+
with _AGENT_CACHE_LOCK:
|
|
1444
|
+
entries = [_AGENT_CACHE.pop(sid) for sid, entry in list(_AGENT_CACHE.items()) if os.path.realpath(entry[2]) == target]
|
|
1445
|
+
started = _release_agents_locked(entries)
|
|
1446
|
+
started += _cancel_background_reviews([(entry[0], entry[2]) for entry in entries], "profile deleted")
|
|
1447
|
+
deadline = time.monotonic() + _PROFILE_RELEASE_TIMEOUT
|
|
1448
|
+
# Non-turn holders keep their agent past the cache; its release starts when the last holder lets go.
|
|
1449
|
+
while (holding := _holding_home(target)) and time.monotonic() < deadline:
|
|
1450
|
+
time.sleep(0.05)
|
|
1451
|
+
# The threads started here count even once they left ``_RELEASES``: a fast failure must still block the delete.
|
|
1452
|
+
with _RELEASES_LOCK:
|
|
1453
|
+
threads = list(dict.fromkeys(started + [thread for thread, owner in _RELEASES.items() if owner is not None and os.path.realpath(owner) == target]))
|
|
1454
|
+
_join(threads, deadline)
|
|
1455
|
+
if any(thread.is_alive() and thread.name.startswith("chat-mcp-") for thread in threads):
|
|
1456
|
+
raise RpcError("The profile is still connecting MCP tools. Retry in a moment.")
|
|
1457
|
+
if holding or any(thread.is_alive() for thread in threads):
|
|
1458
|
+
raise RpcError("The profile is still saving memory from its chats. Retry in a moment.")
|
|
1459
|
+
if any(thread.failed for thread in threads):
|
|
1460
|
+
raise RpcError("Memory from the profile's chats could not be saved; see the sidecar log. Retry to delete the profile anyway.")
|
|
1461
|
+
release_home(target)
|
|
1462
|
+
yield
|
|
1463
|
+
# The old generation has no worker left that could restore its readiness after deletion.
|
|
1464
|
+
forget_profile_mcp_tools(target)
|
|
1465
|
+
with _MCP_DISCOVERY_LOCK:
|
|
1466
|
+
_MCP_DISCOVERIES.pop(target, None)
|
|
1467
|
+
finally:
|
|
1468
|
+
with _MCP_DISCOVERY_LOCK:
|
|
1469
|
+
_MCP_CLOSING_HOMES.discard(target)
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def _holding_home(target: str) -> bool:
|
|
1473
|
+
with _AGENT_CACHE_LOCK:
|
|
1474
|
+
return any(os.path.realpath(home) == target for _agent, home in _COMMITTING)
|
|
766
1475
|
|
|
767
1476
|
|
|
768
1477
|
def _acquire_session_db(home):
|
|
@@ -778,17 +1487,6 @@ def _acquire_session_db(home):
|
|
|
778
1487
|
return None
|
|
779
1488
|
|
|
780
1489
|
|
|
781
|
-
def _release_session_db(db) -> None:
|
|
782
|
-
if db is None:
|
|
783
|
-
return
|
|
784
|
-
try:
|
|
785
|
-
from hermes_state_registry import release_or_close
|
|
786
|
-
|
|
787
|
-
release_or_close(db)
|
|
788
|
-
except Exception: # noqa: BLE001
|
|
789
|
-
log.warning("releasing an agent's state.db handle failed", exc_info=True)
|
|
790
|
-
|
|
791
|
-
|
|
792
1490
|
def _execution_session_id(db, session_id: str) -> str:
|
|
793
1491
|
"""Predecessor ``_resolve_agent_execution_session_id``: a new agent for a session the Agent rotated by compression
|
|
794
1492
|
(the old agent went with a sidecar restart or a runtime change) runs on the live tip, with the server's history,
|
|
@@ -814,6 +1512,20 @@ def _agent_class():
|
|
|
814
1512
|
return AIAgent
|
|
815
1513
|
|
|
816
1514
|
|
|
1515
|
+
def _require_data_use_support(history: list) -> frozenset[str]:
|
|
1516
|
+
"""A downgraded Agent must never send history whose connector privacy requirements it cannot enforce."""
|
|
1517
|
+
if not any(isinstance(row, dict) and row.get("data_use") for row in history):
|
|
1518
|
+
return frozenset()
|
|
1519
|
+
try:
|
|
1520
|
+
from agent.message_metadata import DATA_USE, data_use_requirement
|
|
1521
|
+
except ImportError:
|
|
1522
|
+
pass
|
|
1523
|
+
else:
|
|
1524
|
+
if DATA_USE == "data_use" and callable(data_use_requirement):
|
|
1525
|
+
return data_use_requirement(history)
|
|
1526
|
+
raise RpcError("This Agent release cannot enforce connector privacy requirements. Update Hermes Agent before continuing, compressing or refining this chat.", condition="agent_incompatible")
|
|
1527
|
+
|
|
1528
|
+
|
|
817
1529
|
def _max_iterations_summary_request() -> str:
|
|
818
1530
|
"""The user row the Agent appends to ask for a summary once its tool budget runs out; the server drops it."""
|
|
819
1531
|
try:
|
|
@@ -823,6 +1535,14 @@ def _max_iterations_summary_request() -> str:
|
|
|
823
1535
|
return str(MAX_ITERATIONS_SUMMARY_REQUEST)
|
|
824
1536
|
|
|
825
1537
|
|
|
1538
|
+
def _has_voice_turn_route() -> bool:
|
|
1539
|
+
"""Whether the Agent routes a marked turn to ``auxiliary.voice_chat`` (``agent.voice_turn_route``, v0.21.6)."""
|
|
1540
|
+
try:
|
|
1541
|
+
return importlib.util.find_spec("agent.voice_turn_route") is not None
|
|
1542
|
+
except (ImportError, ValueError):
|
|
1543
|
+
return False
|
|
1544
|
+
|
|
1545
|
+
|
|
826
1546
|
def _supported(cls, name: str) -> bool:
|
|
827
1547
|
"""Whether ``cls.__init__`` accepts ``name`` (named, or through ``**kwargs``)."""
|
|
828
1548
|
try:
|
|
@@ -832,6 +1552,15 @@ def _supported(cls, name: str) -> bool:
|
|
|
832
1552
|
return name in parameters or any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values())
|
|
833
1553
|
|
|
834
1554
|
|
|
1555
|
+
@lru_cache(maxsize=32)
|
|
1556
|
+
def _run_conversation_parameters(cls) -> frozenset[str]:
|
|
1557
|
+
"""Probe explicit turn capabilities once per Agent class; unknown signatures keep legacy arguments."""
|
|
1558
|
+
try:
|
|
1559
|
+
return frozenset(inspect.signature(cls.run_conversation).parameters)
|
|
1560
|
+
except (TypeError, ValueError):
|
|
1561
|
+
return frozenset()
|
|
1562
|
+
|
|
1563
|
+
|
|
835
1564
|
def _approval_notifier(session_id: str, emit):
|
|
836
1565
|
"""The Agent's gateway notify callback for one turn: emits each parked approval, then ``approval_resolved``
|
|
837
1566
|
(id plus the Agent's reason) once its wait ends by any path, an ``approvals.timeout`` expiry included (TAL-514)."""
|
|
@@ -876,20 +1605,51 @@ def _is_compression_start(lower: str) -> bool:
|
|
|
876
1605
|
|
|
877
1606
|
|
|
878
1607
|
def _status_events(kind, text: str) -> list[str]:
|
|
879
|
-
"""Events for one Agent status line:
|
|
1608
|
+
"""Events for one Agent status line: compression, provider diagnostics, and the turn's terminal error."""
|
|
880
1609
|
lower = text.lower()
|
|
881
1610
|
events = ["terminal_error"] if "non-retryable error" in lower else []
|
|
882
|
-
|
|
1611
|
+
kind = str(kind or "").strip().lower()
|
|
1612
|
+
# TAL-1006: the Agent's one terminal edge of a started compaction (``_emit_compaction_done``); an aborted attempt
|
|
1613
|
+
# sends none and ends with the turn.
|
|
1614
|
+
if kind == "compacted":
|
|
1615
|
+
return ["compression_finished"]
|
|
1616
|
+
if kind == "lifecycle":
|
|
883
1617
|
if _is_compression_start(lower):
|
|
884
1618
|
return events + ["compressing"]
|
|
885
1619
|
# The Agent's own compression retries ("Compressed N → M messages, retrying...") are not provider fallbacks.
|
|
886
1620
|
if "compress" in lower or "context reduced to" in lower:
|
|
887
1621
|
return events
|
|
1622
|
+
# Buffered diagnostics change wording across Agent releases; their severity glyph stays stable.
|
|
1623
|
+
if not events and text.startswith(("⚠", "❌")):
|
|
1624
|
+
return ["warning"]
|
|
888
1625
|
if "fallback" in lower or "rate limit" in lower or ("retry" in lower and "non-retryable" not in lower):
|
|
889
1626
|
events.append("warning")
|
|
890
1627
|
return events
|
|
891
1628
|
|
|
892
1629
|
|
|
1630
|
+
#: TAL-821: the Agent's live status line during a rate-limit backoff (``turn_recovery.compute_error_backoff``), e.g.
|
|
1631
|
+
#: "⏳ the free model is busy — resets in ~17m, retrying in 600s (attempt 1/3)". The Agent's other waits are not known
|
|
1632
|
+
#: rate limits: "waiting on provider" covers a 5xx retry as well as a 429 without a reset, and "provider overloaded" is a
|
|
1633
|
+
#: Z.AI overload. ``webui`` is not an attended source, so the Agent sits out a free-tier cooldown instead of ending.
|
|
1634
|
+
_RATE_LIMIT_WAIT_RE = re.compile(r"^⏳ (?:rate limited|the free model is busy) —.* retrying in (\d+)s\b", re.IGNORECASE)
|
|
1635
|
+
#: The line's reset window, in the Agent's ``format_duration_compact`` units ("45s", "17m", "1h 5m", "1.5d").
|
|
1636
|
+
_RESET_IN_RE = re.compile(r"resets in ~((?:\d+(?:\.\d+)?[dhms] ?)+)")
|
|
1637
|
+
_DURATION_UNITS = {"d": 86400, "h": 3600, "m": 60, "s": 1}
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
def _rate_limit_wait(text: str) -> tuple[float, str] | None:
|
|
1641
|
+
"""``(seconds, "reset")`` until the provider's limit resets, else ``(seconds, "retry")`` until the Agent retries, for a
|
|
1642
|
+
rate-limit wait line; None for any other line. The reset is the provider's own ``reset_at`` as the Agent read it from
|
|
1643
|
+
the error; the credential pool's exhausted mark is no better, since without that field it is a fixed one-hour guess."""
|
|
1644
|
+
wait = _RATE_LIMIT_WAIT_RE.match(text)
|
|
1645
|
+
if not wait:
|
|
1646
|
+
return None
|
|
1647
|
+
reset = _RESET_IN_RE.search(text)
|
|
1648
|
+
if not reset:
|
|
1649
|
+
return float(wait.group(1)), "retry"
|
|
1650
|
+
return sum(float(amount) * _DURATION_UNITS[unit] for amount, unit in re.findall(r"(\d+(?:\.\d+)?)([dhms])", reset.group(1))), "reset"
|
|
1651
|
+
|
|
1652
|
+
|
|
893
1653
|
def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn, one function
|
|
894
1654
|
session_id = str(params.get("session_id") or "").strip()
|
|
895
1655
|
stream_id = str(params.get("stream_id") or "").strip()
|
|
@@ -905,10 +1665,16 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
905
1665
|
history = []
|
|
906
1666
|
if not isinstance(history, list):
|
|
907
1667
|
raise InvalidParams("conversation_history must be a list")
|
|
908
|
-
|
|
1668
|
+
_require_data_use_support(history)
|
|
1669
|
+
from hermes_cli.config import load_config_readonly
|
|
1670
|
+
|
|
1671
|
+
# Borrow one snapshot per turn; helpers copy values retained by the Agent.
|
|
1672
|
+
cfg = load_config_readonly() or {}
|
|
1673
|
+
toolsets = _turn_toolsets(params.get("enabled_toolsets"), cfg)
|
|
909
1674
|
system_message = params.get("system_message")
|
|
910
1675
|
home = profile_home_param(params)
|
|
911
1676
|
run = _Run(stream_id, session_id, ctx)
|
|
1677
|
+
run.home = home
|
|
912
1678
|
with _RUNS_LOCK:
|
|
913
1679
|
if _DRAINING.is_set():
|
|
914
1680
|
raise RpcError("The sidecar is shutting down.", condition="sidecar_unavailable")
|
|
@@ -916,13 +1682,22 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
916
1682
|
# A previous turn for this session that never returned (a tool ignoring its interrupt) still owns the cached
|
|
917
1683
|
# agent; this turn must not share, rebind, or un-interrupt that live instance.
|
|
918
1684
|
session_busy = prior is not None and not prior.finished.is_set()
|
|
1685
|
+
# TAL-829: a second agent beside a turn nobody stopped would wait on that turn's durable turn lease (up to 30
|
|
1686
|
+
# minutes) and then write the same session. A stopped turn only lingers until the Agent abandons the wedged
|
|
1687
|
+
# tool, which lets its lease go within seconds.
|
|
1688
|
+
if session_busy and not (prior.cancel.is_set() or prior.ctx.cancelled):
|
|
1689
|
+
raise RpcError("This chat is still answering the previous message. Stop it or wait for it to finish.", condition="session_busy")
|
|
919
1690
|
_RUNS[stream_id] = run
|
|
920
1691
|
_RUNS_BY_SESSION[session_id] = stream_id
|
|
921
1692
|
raw_emit = ctx.emit
|
|
922
1693
|
steer_state = {"last": None}
|
|
923
1694
|
usage_state = {"last": None}
|
|
1695
|
+
pending_mcp = False
|
|
1696
|
+
token_parts: list[str] = []
|
|
1697
|
+
last_token_flush: float | None = None
|
|
1698
|
+
emit_lock = threading.Lock()
|
|
924
1699
|
|
|
925
|
-
def
|
|
1700
|
+
def emit_frame(event, data=None):
|
|
926
1701
|
"""Predecessor ``_webui_steer_events_before``: before content frames, report the Agent's pending steer text
|
|
927
1702
|
whenever it changed so the server can mark consumed steers live rather than only after ``done``. TAL-397: the
|
|
928
1703
|
Agent's session counters ride the same boundary, so the server meters cost and tokens while the turn runs."""
|
|
@@ -936,15 +1711,46 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
936
1711
|
usage_state["last"] = usage
|
|
937
1712
|
raw_emit("usage", usage)
|
|
938
1713
|
raw_emit(event, data)
|
|
1714
|
+
|
|
1715
|
+
def flush_tokens():
|
|
1716
|
+
nonlocal last_token_flush
|
|
1717
|
+
if token_parts:
|
|
1718
|
+
text = "".join(token_parts)
|
|
1719
|
+
token_parts.clear()
|
|
1720
|
+
last_token_flush = time.monotonic()
|
|
1721
|
+
emit_frame("token", {"text": text})
|
|
1722
|
+
|
|
1723
|
+
def emit(event, data=None):
|
|
1724
|
+
# Batch before snapshots/serialization; the lock keeps tool-worker and watcher frames in order.
|
|
1725
|
+
with emit_lock:
|
|
1726
|
+
if event == "token":
|
|
1727
|
+
token_parts.append(data["text"])
|
|
1728
|
+
if last_token_flush is None or time.monotonic() - last_token_flush >= 0.016:
|
|
1729
|
+
flush_tokens()
|
|
1730
|
+
else:
|
|
1731
|
+
flush_tokens()
|
|
1732
|
+
emit_frame(event, data)
|
|
1733
|
+
|
|
1734
|
+
timing = getattr(ctx, "timing", None)
|
|
939
1735
|
try:
|
|
940
|
-
|
|
941
|
-
|
|
1736
|
+
if timing:
|
|
1737
|
+
timing.begin("mcp_discovery")
|
|
1738
|
+
mcp_discovery = _discover_mcp_tools(run)
|
|
1739
|
+
pending_mcp = mcp_discovery is not None
|
|
1740
|
+
if timing:
|
|
1741
|
+
timing.end("mcp_discovery", "cancelled" if ctx.cancelled or run.cancel.is_set() else "timeout" if pending_mcp else run.mcp_discovery_outcome)
|
|
1742
|
+
with timing.span("runtime_resolution") if timing else contextlib.nullcontext():
|
|
1743
|
+
runtime = _resolve_runtime(provider, model)
|
|
1744
|
+
if timing:
|
|
1745
|
+
timing.begin("agent_binding")
|
|
942
1746
|
resolved_model = model or str(runtime.get("model") or "")
|
|
943
1747
|
resolved_provider = provider or runtime.get("provider")
|
|
944
1748
|
AIAgent = _agent_class()
|
|
945
1749
|
live_tool_calls: list[dict] = []
|
|
946
1750
|
started_ids: set[str] = set()
|
|
947
1751
|
completed_ids: set[str] = set()
|
|
1752
|
+
edit_snapshots: dict[str, Any] = {}
|
|
1753
|
+
prepared_metadata: dict[str, dict] = {}
|
|
948
1754
|
token_sent = [False]
|
|
949
1755
|
|
|
950
1756
|
def on_token(text):
|
|
@@ -970,6 +1776,9 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
970
1776
|
return
|
|
971
1777
|
if tid:
|
|
972
1778
|
started_ids.add(tid)
|
|
1779
|
+
snapshot = _edit_snapshot(name, args, session_id)
|
|
1780
|
+
if snapshot is not None:
|
|
1781
|
+
edit_snapshots[tid] = snapshot
|
|
973
1782
|
live_tool_calls.append({"name": name, "args": args if isinstance(args, dict) else {}, "tid": tid, "done": False})
|
|
974
1783
|
emit("tool", {"event_type": "tool.started", "name": name, "preview": None, "args": _args_snapshot(args), "tid": tid})
|
|
975
1784
|
|
|
@@ -988,8 +1797,12 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
988
1797
|
call["snippet"] = snippet
|
|
989
1798
|
break
|
|
990
1799
|
payload = {"event_type": "tool.completed", "name": name, "preview": snippet, "args": _args_snapshot(args), "tid": tid, "raw_result": _raw_result(function_result)}
|
|
1800
|
+
# TAL-909: the metadata the Agent persisted on the result row, or, from an Agent without the callback, live only.
|
|
1801
|
+
metadata = prepared_metadata.pop(tid) if tid in prepared_metadata else _edit_metadata(name, args, function_result, edit_snapshots.pop(tid, None))
|
|
1802
|
+
if metadata:
|
|
1803
|
+
payload["display_metadata"] = metadata
|
|
991
1804
|
# TAL-397: ``raw_result`` caps nested values, so the todo tool's list travels whole for the server's ``todo_state``.
|
|
992
|
-
if
|
|
1805
|
+
if function_result is not None and _is_todo_tool(name):
|
|
993
1806
|
payload["todo_result"] = function_result if isinstance(function_result, str) else json.dumps(function_result, default=str)
|
|
994
1807
|
# TAL-448: likewise a result's unified ``diff``, so the server's ``edit_diff`` counts cover the whole change.
|
|
995
1808
|
diff = _result_diff(function_result)
|
|
@@ -1004,8 +1817,21 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1004
1817
|
break
|
|
1005
1818
|
emit("tool_complete", payload)
|
|
1006
1819
|
|
|
1820
|
+
def on_tool_result_metadata(tool_call_id, name, args, function_result):
|
|
1821
|
+
"""TAL-909: called before the Agent appends and flushes the tool-result row, which keeps the returned dict as
|
|
1822
|
+
its ``display_metadata``; the completion frame that follows carries the same dict."""
|
|
1823
|
+
tid = str(tool_call_id or "")
|
|
1824
|
+
metadata = _edit_metadata(name, args, function_result, edit_snapshots.pop(tid, None))
|
|
1825
|
+
prepared_metadata[tid] = metadata
|
|
1826
|
+
return metadata
|
|
1827
|
+
|
|
1007
1828
|
def on_tool(*cb_args, **cb_kwargs):
|
|
1008
|
-
# Structured callbacks carry tool cards; the
|
|
1829
|
+
# Structured callbacks carry tool cards; the Agent already throttles MCP progress.
|
|
1830
|
+
if len(cb_args) >= 3 and cb_args[0] == "tool.progress":
|
|
1831
|
+
emit("tool_progress", {"tool_call_id": str(cb_kwargs.get("tool_call_id") or ""), "message": cb_args[2],
|
|
1832
|
+
"progress": cb_kwargs.get("progress"), "total": cb_kwargs.get("total"),
|
|
1833
|
+
"percent": cb_kwargs.get("percent")})
|
|
1834
|
+
return
|
|
1009
1835
|
if len(cb_args) >= 3 and cb_args[0] in ("reasoning.available", "_thinking"):
|
|
1010
1836
|
text = cb_args[2] if cb_args[0] == "reasoning.available" else cb_args[1]
|
|
1011
1837
|
if text:
|
|
@@ -1016,43 +1842,172 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1016
1842
|
for event in _status_events(kind, text):
|
|
1017
1843
|
if event == "compressing":
|
|
1018
1844
|
emit("compressing", {"session_id": session_id, "message": "Compressing context"})
|
|
1845
|
+
elif event == "compression_finished":
|
|
1846
|
+
emit("compression_finished", {"session_id": session_id})
|
|
1019
1847
|
elif event == "warning":
|
|
1020
1848
|
emit("warning", {"type": "fallback", "message": text})
|
|
1021
1849
|
else:
|
|
1022
1850
|
emit("status", {"kind": "terminal_error", "message": text})
|
|
1023
1851
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1852
|
+
wait_state = {"active": False, "prompt": False}
|
|
1853
|
+
|
|
1854
|
+
def on_thinking(text):
|
|
1855
|
+
"""Normalize provider waits and prompt-reading progress from the Agent's live status callback."""
|
|
1856
|
+
line = str(text or "").strip()
|
|
1857
|
+
progress = re.fullmatch(r"⚙ processing prompt(?: — (\d+(?:\.\d+)?)%)?", line)
|
|
1858
|
+
if progress:
|
|
1859
|
+
wait_state["prompt"] = True
|
|
1860
|
+
emit("provider_phase", {"phase": {"phase": "prompt_reading", "progress_percent": min(100, float(progress[1])) if progress[1] else None}})
|
|
1861
|
+
elif wait_state["prompt"]:
|
|
1862
|
+
wait_state["prompt"] = False
|
|
1863
|
+
emit("provider_phase", {"phase": None})
|
|
1864
|
+
wait = _rate_limit_wait(line)
|
|
1865
|
+
if wait is not None:
|
|
1866
|
+
wait_state["active"] = True
|
|
1867
|
+
emit("rate_limit_wait", {"provider": str(getattr(run.agent, "provider", None) or resolved_provider or "") or None,
|
|
1868
|
+
"until": time.time() + wait[0], "until_kind": wait[1]})
|
|
1869
|
+
elif wait_state["active"]:
|
|
1870
|
+
wait_state["active"] = False
|
|
1871
|
+
emit("rate_limit_wait", {"provider": None, "until": None})
|
|
1872
|
+
|
|
1873
|
+
# TAL-803: the Agent's sticky ``AgentNotice``s (a startup warning such as a memory provider that failed to load,
|
|
1874
|
+
# credit and usage bands) and their clears; the server keeps the session's active set.
|
|
1875
|
+
def on_notice(notice):
|
|
1876
|
+
text = str(getattr(notice, "text", "") or "").strip()
|
|
1877
|
+
if not text:
|
|
1878
|
+
return
|
|
1879
|
+
ttl_ms = getattr(notice, "ttl_ms", None)
|
|
1880
|
+
key = getattr(notice, "key", None)
|
|
1881
|
+
# The Agent raises a startup warning (memory provider unavailable) once per process, so a rebuilt agent never
|
|
1882
|
+
# repeats it: it outlives the agent that raised it. Credit notices are per agent and re-raised by the next one.
|
|
1883
|
+
emit("notice", {"id": getattr(notice, "id", None), "key": key, "level": str(getattr(notice, "level", "") or "info"),
|
|
1884
|
+
"kind": str(getattr(notice, "kind", "") or "sticky"), "ttl_ms": ttl_ms if isinstance(ttl_ms, int) else None, "text": text,
|
|
1885
|
+
"lapses_with_agent": not str(key or "").startswith("startup-warning.")})
|
|
1886
|
+
|
|
1887
|
+
def on_notice_clear(key):
|
|
1888
|
+
if key:
|
|
1889
|
+
emit("notice_clear", {"key": str(key)})
|
|
1890
|
+
|
|
1891
|
+
def ask(data: dict, timeout: int) -> str | None:
|
|
1892
|
+
"""Park on a prompt the server relays until it is answered, the turn is cancelled or ends, or ``timeout``
|
|
1893
|
+
passes (``<= 0``: no limit). The answer, or None, goes back to the Agent and is never emitted."""
|
|
1036
1894
|
entry = _ClarifyEntry(data)
|
|
1037
1895
|
entry.data["clarify_id"] = entry.clarify_id
|
|
1896
|
+
entry.deadline = None if timeout <= 0 else time.monotonic() + float(timeout)
|
|
1038
1897
|
with run.lock:
|
|
1039
1898
|
run.clarify_entries[entry.clarify_id] = entry
|
|
1040
1899
|
emit("clarify", dict(entry.data))
|
|
1041
|
-
deadline = None if timeout <= 0 else time.monotonic() + float(timeout)
|
|
1042
1900
|
while not entry.event.is_set():
|
|
1043
|
-
if run.cancel.is_set() or run.finished.is_set():
|
|
1901
|
+
if run.cancel.is_set() or run.finished.is_set() or (data.get("kind") == "skill_secret" and (ctx.cancelled or run.session_ended)):
|
|
1044
1902
|
break
|
|
1045
|
-
if deadline is None:
|
|
1903
|
+
if entry.deadline is None:
|
|
1046
1904
|
entry.event.wait(1.0)
|
|
1047
1905
|
continue
|
|
1048
|
-
remaining = deadline - time.monotonic()
|
|
1906
|
+
remaining = entry.deadline - time.monotonic()
|
|
1049
1907
|
if remaining <= 0:
|
|
1050
1908
|
break
|
|
1051
1909
|
entry.event.wait(min(1.0, remaining))
|
|
1052
1910
|
with run.lock:
|
|
1053
1911
|
run.clarify_entries.pop(entry.clarify_id, None)
|
|
1054
1912
|
emit("clarify_resolved", {"clarify_id": entry.clarify_id, "session_id": session_id})
|
|
1055
|
-
return entry.result
|
|
1913
|
+
return entry.result
|
|
1914
|
+
|
|
1915
|
+
def ask_vault(kind: str, fields: dict) -> str:
|
|
1916
|
+
"""TAL-887: a password-manager prompt; an empty answer is the Agent's decline, as are expiry and cancel."""
|
|
1917
|
+
timeout = _VAULT_PROMPT_TIMEOUTS[kind]
|
|
1918
|
+
data = {"kind": kind, "session_id": session_id, "requested_at": time.time(), "timeout_seconds": timeout, **fields}
|
|
1919
|
+
return ask(data, timeout) or ""
|
|
1920
|
+
|
|
1921
|
+
def vault_unlock_prompt(backend, display_name):
|
|
1922
|
+
# The Agent stores the unlock right after this returns, on the thread that called it: a tool worker
|
|
1923
|
+
# inherits this callback but not the turn's session, so bind it here too, or closing the session could
|
|
1924
|
+
# not lock the manager again.
|
|
1925
|
+
_set_vault_session(session_id)
|
|
1926
|
+
return ask_vault("vault_unlock", {"backend": str(backend), "display_name": str(display_name)})
|
|
1927
|
+
|
|
1928
|
+
def vault_save_login_prompt(origin, site):
|
|
1929
|
+
# The server sends ``{"identifier", "password"}``; the Agent stores it in its vault and fills the page.
|
|
1930
|
+
raw = ask_vault("vault_save_login", {"origin": str(origin), "site": str(site)})
|
|
1931
|
+
try:
|
|
1932
|
+
answer = json.loads(raw) if raw else None
|
|
1933
|
+
except ValueError:
|
|
1934
|
+
return None
|
|
1935
|
+
return answer if isinstance(answer, dict) and answer.get("password") else None
|
|
1936
|
+
|
|
1937
|
+
def vault_code_prompt(site, hint):
|
|
1938
|
+
return ask_vault("vault_code", {"site": str(site), "hint": str(hint or "")})
|
|
1939
|
+
|
|
1940
|
+
def skill_secret_prompt(env_var, prompt, metadata=None):
|
|
1941
|
+
skipped = {"success": True, "stored_as": env_var, "validated": False, "skipped": True}
|
|
1942
|
+
if not isinstance(env_var, str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", env_var):
|
|
1943
|
+
return skipped
|
|
1944
|
+
metadata = metadata if isinstance(metadata, dict) else {}
|
|
1945
|
+
fields = {"env_var": env_var, "prompt": str(prompt), **{key: str(metadata.get(key) or "") for key in ("skill_name", "help", "required_for")}}
|
|
1946
|
+
value = ask_vault("skill_secret", fields)
|
|
1947
|
+
if not value.strip():
|
|
1948
|
+
return skipped
|
|
1949
|
+
# The native writer strips these bytes, and reports non-ASCII characters to stderr. Refuse rather
|
|
1950
|
+
# than change the credential or expose any of its characters in logs.
|
|
1951
|
+
if not value.isascii() or any(char in value for char in ("\r", "\n", "\0")):
|
|
1952
|
+
return {**skipped, "success": False}
|
|
1953
|
+
try:
|
|
1954
|
+
from hermes_cli.config import save_env_value_secure
|
|
1955
|
+
from hermes_constants import get_hermes_home
|
|
1956
|
+
|
|
1957
|
+
# Keep the inherited secret scope so setup can use the value immediately. Fence the write against
|
|
1958
|
+
# eviction/replacement under the same lock that changes the run's identity and session lifetime.
|
|
1959
|
+
with _RUNS_LOCK:
|
|
1960
|
+
if (ctx.cancelled or run.cancel.is_set() or run.finished.is_set() or run.session_ended
|
|
1961
|
+
or _RUNS_BY_SESSION.get(session_id) != stream_id or _RUNS.get(stream_id) is not run
|
|
1962
|
+
or get_hermes_home() != home):
|
|
1963
|
+
return skipped
|
|
1964
|
+
saved = save_env_value_secure(env_var, value)
|
|
1965
|
+
return {"success": saved.get("success") is True, "stored_as": env_var, "validated": saved.get("validated") is True, "skipped": False}
|
|
1966
|
+
except Exception: # noqa: BLE001 - never expose an exception that could include the submitted value
|
|
1967
|
+
return {**skipped, "success": False}
|
|
1968
|
+
|
|
1969
|
+
def clarify_callback(question, choices=None, multi_select=False, questions=None):
|
|
1970
|
+
# TAL-351: remove the legacy signature path when v0.21.6 is the oldest supported Agent.
|
|
1971
|
+
structured = isinstance(question, list)
|
|
1972
|
+
if structured:
|
|
1973
|
+
questions, question = question, ""
|
|
1974
|
+
choices_list = [str(c) for c in (choices or [])]
|
|
1975
|
+
# Predecessor `_clarify_timeout_seconds`: the Agent's own resolver over this profile's config
|
|
1976
|
+
# (``clarify.timeout`` else ``agent.clarify_timeout`` else 3600); ``<= 0`` waits until answered or cancelled.
|
|
1977
|
+
# The advertised ``timeout_seconds`` is what the clients count down, so it is the same number.
|
|
1978
|
+
timeout = _clarify_timeout(params)
|
|
1979
|
+
data = {"question": str(question or ""), "choices_offered": choices_list, "session_id": session_id, "kind": "clarify", "requested_at": time.time(), "timeout_seconds": timeout}
|
|
1980
|
+
if multi_select:
|
|
1981
|
+
data["multi_select"] = True
|
|
1982
|
+
# Batch prompts (the Agent's ``_run_batch``) carry no top-level question and expect the ``{"answers": {...}}`` reply.
|
|
1983
|
+
if isinstance(questions, list) and questions:
|
|
1984
|
+
data["questions"] = questions
|
|
1985
|
+
answer = ask(data, timeout)
|
|
1986
|
+
if answer is not None:
|
|
1987
|
+
if structured:
|
|
1988
|
+
return {"answers": json.loads(answer)["answers"], "outcome": "submitted"}
|
|
1989
|
+
return answer
|
|
1990
|
+
cancelled = run.cancel.is_set() or run.finished.is_set()
|
|
1991
|
+
if structured:
|
|
1992
|
+
return {"answers": {}, "outcome": "cancelled" if cancelled else "timed_out",
|
|
1993
|
+
"notice": "The user cancelled the clarification." if cancelled else "The clarification timed out."}
|
|
1994
|
+
# Older tools have no outcome field. A batch understands timed_out; a single needs an empty answer.
|
|
1995
|
+
if questions:
|
|
1996
|
+
return json.dumps({"answers": {}, "timed_out": not cancelled})
|
|
1997
|
+
return ""
|
|
1998
|
+
|
|
1999
|
+
# TAL-818: a ``manage_connections`` card (a hosted connect, an MCP install) opened; the server turns the frame into
|
|
2000
|
+
# the chat's connect card, which reads and answers the operation through ``connectors.operation`` / ``respond``.
|
|
2001
|
+
# Only a client that draws the card asks for it: without the callback the Agent answers with plain links at once.
|
|
2002
|
+
connect_card = params.get("connect_card") is True
|
|
2003
|
+
|
|
2004
|
+
def on_connection(payload):
|
|
2005
|
+
from .connectors import remember_chat_operation
|
|
2006
|
+
|
|
2007
|
+
view = remember_chat_operation(home, payload)
|
|
2008
|
+
if view is not None:
|
|
2009
|
+
emit("connection", view)
|
|
2010
|
+
return None
|
|
1056
2011
|
|
|
1057
2012
|
# Approval bridge: the Agent parks dangerous commands in its gateway queue and calls back here.
|
|
1058
2013
|
try:
|
|
@@ -1061,51 +2016,21 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1061
2016
|
register_gateway_notify = unregister_gateway_notify = None
|
|
1062
2017
|
approval_cb = _approval_notifier(session_id, emit) if register_gateway_notify is not None else None
|
|
1063
2018
|
|
|
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
|
-
)
|
|
2019
|
+
kwargs = _agent_kwargs(AIAgent, params, session_id, resolved_model, resolved_provider, runtime, toolsets, cfg)
|
|
2020
|
+
kwargs.update(stream_delta_callback=on_token, reasoning_callback=on_reasoning, tool_progress_callback=on_tool, clarify_callback=clarify_callback)
|
|
1078
2021
|
for name, value in (
|
|
1079
2022
|
("interim_assistant_callback", on_interim),
|
|
1080
2023
|
("tool_start_callback", on_tool_start),
|
|
1081
2024
|
("tool_complete_callback", on_tool_complete),
|
|
2025
|
+
("tool_result_metadata_callback", on_tool_result_metadata),
|
|
1082
2026
|
("status_callback", on_status),
|
|
1083
|
-
("
|
|
1084
|
-
("
|
|
1085
|
-
("
|
|
1086
|
-
("
|
|
1087
|
-
("gateway_session_key", session_id),
|
|
2027
|
+
("thinking_callback", on_thinking),
|
|
2028
|
+
("notice_callback", on_notice),
|
|
2029
|
+
("notice_clear_callback", on_notice_clear),
|
|
2030
|
+
("connection_callback", on_connection if connect_card else None),
|
|
1088
2031
|
):
|
|
1089
2032
|
if _supported(AIAgent, name) and value is not None:
|
|
1090
2033
|
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
2034
|
signature = _agent_signature(resolved_model, resolved_provider, runtime, toolsets, str(params.get("profile_home")), kwargs)
|
|
1110
2035
|
agent = None
|
|
1111
2036
|
|
|
@@ -1113,6 +2038,8 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1113
2038
|
"""Claim the agent for this run under ``_AGENT_CACHE_LOCK``, so an eviction never releases it mid-turn."""
|
|
1114
2039
|
run.prior_messages = getattr(bound, "_session_messages", None)
|
|
1115
2040
|
run.agent = bound
|
|
2041
|
+
if run.session_ended:
|
|
2042
|
+
bound.skip_background_review = True
|
|
1116
2043
|
|
|
1117
2044
|
if not session_busy:
|
|
1118
2045
|
with _AGENT_CACHE_LOCK:
|
|
@@ -1121,28 +2048,36 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1121
2048
|
agent = cached[0]
|
|
1122
2049
|
_AGENT_CACHE.move_to_end(session_id)
|
|
1123
2050
|
bind(agent)
|
|
2051
|
+
cached_agent = agent is not None
|
|
1124
2052
|
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"):
|
|
2053
|
+
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
2054
|
if name in kwargs and hasattr(agent, name):
|
|
1127
2055
|
setattr(agent, name, kwargs[name])
|
|
2056
|
+
# A cached agent keeps the last turn's card callback; this turn's client decides afresh (TAL-818).
|
|
2057
|
+
if hasattr(agent, "connection_callback"):
|
|
2058
|
+
agent.connection_callback = on_connection if connect_card else None
|
|
2059
|
+
# TAL-902: an agent ``/refine`` built runs its first turn here. Like a new one, it lapses the notices of the
|
|
2060
|
+
# agent it replaced and raises the startup notices it parked while it had no callback.
|
|
2061
|
+
if vars(agent).pop("_talaria_unannounced", False):
|
|
2062
|
+
emit("notices_reset", {})
|
|
2063
|
+
replay = getattr(agent, "_replay_startup_warnings", None)
|
|
2064
|
+
if callable(replay):
|
|
2065
|
+
replay()
|
|
1128
2066
|
else:
|
|
2067
|
+
# TAL-803: the notices a replaced agent raised lapse with it (``lapses_with_agent``); this one raises its own.
|
|
2068
|
+
emit("notices_reset", {})
|
|
1129
2069
|
# 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``)
|
|
2070
|
+
agent = _build_agent(AIAgent, kwargs, home, session_id, state_db=not params.get("ephemeral"))
|
|
1140
2071
|
with _AGENT_CACHE_LOCK:
|
|
1141
2072
|
bind(agent)
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
2073
|
+
if not run.session_ended:
|
|
2074
|
+
replaced = _AGENT_CACHE.pop(session_id, None)
|
|
2075
|
+
_AGENT_CACHE[session_id] = (agent, signature, home)
|
|
2076
|
+
_release_agents_locked([replaced] if replaced else [])
|
|
2077
|
+
_evict_idle_agents_locked()
|
|
2078
|
+
if timing:
|
|
2079
|
+
timing.end("agent_binding", "cache_hit" if cached_agent else "ok")
|
|
2080
|
+
timing.begin("conversation_setup")
|
|
1146
2081
|
# Predecessor ``agent.ephemeral_system_prompt``: personality, surface context, progress guidance, and delivery
|
|
1147
2082
|
# hints travel as runtime instructions that are never persisted to history.
|
|
1148
2083
|
ephemeral = params.get("ephemeral_system_prompt")
|
|
@@ -1168,7 +2103,19 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1168
2103
|
log.debug("agent.clear_interrupt failed", exc_info=True)
|
|
1169
2104
|
|
|
1170
2105
|
def _watch_cancel():
|
|
2106
|
+
nonlocal pending_mcp
|
|
1171
2107
|
while not run.cancel.wait(0.25):
|
|
2108
|
+
if pending_mcp and mcp_discovery.is_set():
|
|
2109
|
+
# Discovery can finish while this agent is being built, after the worker's cache refresh.
|
|
2110
|
+
try:
|
|
2111
|
+
with scoped_home(home):
|
|
2112
|
+
refresh_profile_agents_mcp_tools(home)
|
|
2113
|
+
except Exception: # noqa: BLE001 - refresh failure must not disable Stop
|
|
2114
|
+
log.warning("Late MCP refresh failed", exc_info=True)
|
|
2115
|
+
with run.lock:
|
|
2116
|
+
if pending_mcp:
|
|
2117
|
+
emit("notice_clear", {"key": _MCP_PENDING_KEY})
|
|
2118
|
+
pending_mcp = False
|
|
1172
2119
|
if run.finished.is_set():
|
|
1173
2120
|
return
|
|
1174
2121
|
if ctx.cancelled:
|
|
@@ -1189,27 +2136,44 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1189
2136
|
watcher = threading.Thread(target=_watch_cancel, daemon=True, name=f"chat-cancel-{stream_id[:8]}")
|
|
1190
2137
|
watcher.start()
|
|
1191
2138
|
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()
|
|
2139
|
+
run_params = _run_conversation_parameters(type(agent))
|
|
1196
2140
|
if system_message and ("system_message" in run_params or not run_params):
|
|
1197
2141
|
run_kwargs["system_message"] = system_message
|
|
2142
|
+
# Talaria generates and syncs the title; suppress the Agent's duplicate auxiliary request.
|
|
2143
|
+
if "title_user_message" in run_params:
|
|
2144
|
+
run_kwargs["title_user_message"] = ""
|
|
1198
2145
|
# The server's clean user text (no workspace prefix) is what state.db stores; the Agent keeps the sent bytes beside it.
|
|
1199
2146
|
persist = params.get("persist_user_message")
|
|
1200
2147
|
if not (isinstance(persist, str) and persist.strip()):
|
|
1201
2148
|
persist = user_message if isinstance(user_message, str) else None
|
|
1202
2149
|
if "persist_user_message" in run_params and persist is not None:
|
|
1203
2150
|
run_kwargs["persist_user_message"] = persist
|
|
2151
|
+
# TAL-801: a spoken turn runs on the profile's ``auxiliary.voice_chat`` slot; the Agent consumes the mark at turn
|
|
2152
|
+
# setup. Every turn sets it, as the Agent's own surfaces do, so a mark a failed turn never consumed cannot leak.
|
|
2153
|
+
if _has_voice_turn_route():
|
|
2154
|
+
agent._voice_turn_pending = params.get("voice_turn") is True
|
|
1204
2155
|
error: str | None = None
|
|
1205
2156
|
result: dict = {}
|
|
2157
|
+
# The Agent reads its vault prompts from the thread running the turn (and copies them onto tool workers).
|
|
2158
|
+
_set_vault_prompts(session_id, vault_unlock_prompt, vault_save_login_prompt, vault_code_prompt)
|
|
1206
2159
|
try:
|
|
1207
|
-
|
|
2160
|
+
try:
|
|
2161
|
+
from tools.skills_tool import bind_turn_secret_capture
|
|
2162
|
+
except ImportError: # Older Agents and v0.22.2 have only an unsafe process-global callback slot.
|
|
2163
|
+
bind_turn_secret_capture = None
|
|
2164
|
+
log.debug("Agent lacks turn-local skill secret capture; secure setup is unavailable")
|
|
2165
|
+
with bind_turn_secret_capture(skill_secret_prompt) if bind_turn_secret_capture and params.get("skill_secret") is True else contextlib.nullcontext():
|
|
2166
|
+
if timing:
|
|
2167
|
+
timing.end("conversation_setup")
|
|
2168
|
+
timing.observe("conversation_entry")
|
|
2169
|
+
timing.emit("conversation_entry")
|
|
2170
|
+
raw = agent.run_conversation(**run_kwargs)
|
|
1208
2171
|
result = raw if isinstance(raw, dict) else {}
|
|
1209
2172
|
except Exception as exc: # noqa: BLE001
|
|
1210
2173
|
log.warning("agent turn failed for %s: %s", session_id, exc)
|
|
1211
2174
|
error = f"{type(exc).__name__}: {exc}"
|
|
1212
2175
|
finally:
|
|
2176
|
+
_set_vault_prompts(None, None, None, None)
|
|
1213
2177
|
run.finished.set()
|
|
1214
2178
|
watcher.join(timeout=2)
|
|
1215
2179
|
# A successor turn for the same session (admitted past the cancel-unwind ceiling) may have registered
|
|
@@ -1255,7 +2219,8 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1255
2219
|
"max_iterations_summary_request": _max_iterations_summary_request(),
|
|
1256
2220
|
"usage": _usage(agent),
|
|
1257
2221
|
"context": _context_length(agent),
|
|
1258
|
-
|
|
2222
|
+
# TAL-801: the Agent's result names the model the turn ran on; a voice turn's route is undone by now.
|
|
2223
|
+
"model": str(result.get("model") or getattr(agent, "model", None) or resolved_model),
|
|
1259
2224
|
"provider": str(resolved_provider or ""),
|
|
1260
2225
|
"compressed": compressed,
|
|
1261
2226
|
"agent_session_id": str(getattr(agent, "session_id", None) or session_id),
|
|
@@ -1264,11 +2229,21 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
|
|
|
1264
2229
|
"live_tool_calls": live_tool_calls,
|
|
1265
2230
|
}
|
|
1266
2231
|
finally:
|
|
2232
|
+
with emit_lock:
|
|
2233
|
+
flush_tokens()
|
|
2234
|
+
with run.lock:
|
|
2235
|
+
if pending_mcp:
|
|
2236
|
+
emit("notice_clear", {"key": _MCP_PENDING_KEY})
|
|
2237
|
+
pending_mcp = False
|
|
1267
2238
|
with _AGENT_CACHE_LOCK:
|
|
1268
2239
|
with _RUNS_LOCK:
|
|
1269
2240
|
_RUNS.pop(stream_id, None)
|
|
1270
2241
|
if _RUNS_BY_SESSION.get(session_id) == stream_id:
|
|
1271
2242
|
_RUNS_BY_SESSION.pop(session_id, None)
|
|
2243
|
+
# TAL-887: a session ended mid-turn (``chat.evict_agent`` marks the run under these locks) released its
|
|
2244
|
+
# unlocks then; one the Agent stored after that is released now.
|
|
2245
|
+
if run.session_ended:
|
|
2246
|
+
_release_vault_session(session_id)
|
|
1272
2247
|
# An agent evicted or replaced while this turn held it is released now that the turn is over.
|
|
1273
2248
|
_release_if_dropped_locked(run.agent, home)
|
|
1274
2249
|
|
|
@@ -1333,6 +2308,7 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1333
2308
|
history = params.get("conversation_history")
|
|
1334
2309
|
if not isinstance(history, list):
|
|
1335
2310
|
raise InvalidParams("conversation_history must be a list")
|
|
2311
|
+
_require_data_use_support(history)
|
|
1336
2312
|
model = str(params.get("model") or "").strip()
|
|
1337
2313
|
provider = str(params.get("model_provider") or "").strip() or None
|
|
1338
2314
|
focus_topic = str(params.get("focus_topic") or "").strip()[:500] or None
|
|
@@ -1351,6 +2327,7 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1351
2327
|
platform="webui",
|
|
1352
2328
|
quiet_mode=True,
|
|
1353
2329
|
enabled_toolsets=toolsets,
|
|
2330
|
+
disabled_toolsets=disabled_toolsets(),
|
|
1354
2331
|
session_id=session_id,
|
|
1355
2332
|
)
|
|
1356
2333
|
for name, value in (
|
|
@@ -1360,6 +2337,8 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1360
2337
|
("credential_pool", runtime.get("credential_pool")),
|
|
1361
2338
|
("gateway_session_key", session_id),
|
|
1362
2339
|
("skip_memory", not _checkpoint_required()),
|
|
2340
|
+
("request_overrides", _request_overrides(kwargs["model"], kwargs["provider"], runtime)),
|
|
2341
|
+
("prefill_messages", _profile_prefill_messages()),
|
|
1363
2342
|
):
|
|
1364
2343
|
if _supported(AIAgent, name) and value is not None:
|
|
1365
2344
|
kwargs[name] = value
|
|
@@ -1398,6 +2377,94 @@ def compress(ctx: CallContext, params: dict) -> dict:
|
|
|
1398
2377
|
_release_compression(agent, committed=False)
|
|
1399
2378
|
|
|
1400
2379
|
|
|
2380
|
+
_REFINE_BUSY = "Agent is running — wait for the turn to finish, then /refine."
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def _session_running_locked(session_id: str) -> bool:
|
|
2384
|
+
"""Whether a turn for ``session_id`` has not returned yet, or no turn can start (caller holds ``_RUNS_LOCK``)."""
|
|
2385
|
+
run = _RUNS.get(_RUNS_BY_SESSION.get(session_id) or "")
|
|
2386
|
+
return _DRAINING.is_set() or (run is not None and not run.finished.is_set())
|
|
2387
|
+
|
|
2388
|
+
|
|
2389
|
+
def _refine_agent(params: dict, session_id: str, home):
|
|
2390
|
+
"""The session's cached agent, else one built the way its turn builds it and cached for that turn, held in
|
|
2391
|
+
``_COMMITTING`` until the caller lets go; None when a turn started meanwhile. Runs under the call's ``scoped_home``."""
|
|
2392
|
+
with _AGENT_CACHE_LOCK:
|
|
2393
|
+
with _RUNS_LOCK:
|
|
2394
|
+
# Checked again with the claim: a turn admitted since ``refine``'s check holds the cached agent.
|
|
2395
|
+
if _session_running_locked(session_id):
|
|
2396
|
+
return None
|
|
2397
|
+
cached = _AGENT_CACHE.get(session_id)
|
|
2398
|
+
if cached is not None:
|
|
2399
|
+
_COMMITTING.append((cached[0], home))
|
|
2400
|
+
return cached[0]
|
|
2401
|
+
model = str(params.get("model") or "").strip()
|
|
2402
|
+
provider = str(params.get("model_provider") or "").strip() or None
|
|
2403
|
+
toolsets = _turn_toolsets(params.get("enabled_toolsets"))
|
|
2404
|
+
# Like a turn: the profile's MCP servers connect before the agent snapshots its tools, which its next turn keeps.
|
|
2405
|
+
try:
|
|
2406
|
+
from .commands import discover_profile_mcp_tools
|
|
2407
|
+
|
|
2408
|
+
discover_profile_mcp_tools()
|
|
2409
|
+
except Exception: # noqa: BLE001
|
|
2410
|
+
log.warning("MCP discovery failed", exc_info=True)
|
|
2411
|
+
runtime = _resolve_runtime(provider, model)
|
|
2412
|
+
resolved_model = model or str(runtime.get("model") or "")
|
|
2413
|
+
resolved_provider = provider or runtime.get("provider")
|
|
2414
|
+
AIAgent = _agent_class()
|
|
2415
|
+
kwargs = _agent_kwargs(AIAgent, params, session_id, resolved_model, resolved_provider, runtime, toolsets)
|
|
2416
|
+
signature = _agent_signature(resolved_model, resolved_provider, runtime, toolsets, str(params.get("profile_home")), kwargs)
|
|
2417
|
+
built = _build_agent(AIAgent, kwargs, home, session_id)
|
|
2418
|
+
with _AGENT_CACHE_LOCK:
|
|
2419
|
+
with _RUNS_LOCK:
|
|
2420
|
+
running = _session_running_locked(session_id)
|
|
2421
|
+
cached = _AGENT_CACHE.get(session_id)
|
|
2422
|
+
agent = None if running else cached[0] if cached else built
|
|
2423
|
+
if agent is not None:
|
|
2424
|
+
_COMMITTING.append((agent, home))
|
|
2425
|
+
if agent is built:
|
|
2426
|
+
built._talaria_unannounced = True # its first turn announces it (``start``)
|
|
2427
|
+
_AGENT_CACHE[session_id] = (built, signature, home)
|
|
2428
|
+
_evict_idle_agents_locked()
|
|
2429
|
+
else:
|
|
2430
|
+
_release_agents_locked([(built, None, home)])
|
|
2431
|
+
return agent
|
|
2432
|
+
|
|
2433
|
+
|
|
2434
|
+
def refine(params: dict) -> str:
|
|
2435
|
+
"""TAL-902: ``/refine [focus]`` like the Agent's live-session handler (``_format_live_refine_output``): the memory/skill
|
|
2436
|
+
review fork of ``messages`` (the server's model context for the session) on the session's agent, never while its turn
|
|
2437
|
+
runs. The fork carries this call's ``scoped_home``, so its memory and skill writes land in the session's profile."""
|
|
2438
|
+
session_id = str(params.get("session_id") or "").strip()
|
|
2439
|
+
messages = params.get("messages")
|
|
2440
|
+
if not session_id or not isinstance(messages, list):
|
|
2441
|
+
raise InvalidParams("session_id and messages are required")
|
|
2442
|
+
_require_data_use_support(messages)
|
|
2443
|
+
focus = str(params.get("focus") or "").strip()
|
|
2444
|
+
with _RUNS_LOCK:
|
|
2445
|
+
if _session_running_locked(session_id):
|
|
2446
|
+
return _REFINE_BUSY
|
|
2447
|
+
if not messages:
|
|
2448
|
+
return "Nothing to refine yet — the conversation is empty."
|
|
2449
|
+
home = profile_home_param(params)
|
|
2450
|
+
agent = _refine_agent(params, session_id, home)
|
|
2451
|
+
if agent is None:
|
|
2452
|
+
return _REFINE_BUSY
|
|
2453
|
+
try:
|
|
2454
|
+
agent._spawn_background_review(messages_snapshot=list(messages), review_memory=True,
|
|
2455
|
+
review_skills="skill_manage" in (getattr(agent, "valid_tool_names", None) or set()),
|
|
2456
|
+
focus=focus or None, explicit=True)
|
|
2457
|
+
except Exception as exc: # noqa: BLE001 - the Agent's own wording for a fork that could not start
|
|
2458
|
+
log.warning("/refine failed to start for session %s", session_id, exc_info=True)
|
|
2459
|
+
return f"/refine failed to start: {exc}"
|
|
2460
|
+
finally:
|
|
2461
|
+
with _AGENT_CACHE_LOCK:
|
|
2462
|
+
_COMMITTING.remove((agent, home))
|
|
2463
|
+
_release_if_dropped_locked(agent, home)
|
|
2464
|
+
tail = f" (focus: {focus})" if focus else ""
|
|
2465
|
+
return f"⚗ Reviewing this conversation in the background{tail} — any memory/skill updates will be reported when done."
|
|
2466
|
+
|
|
2467
|
+
|
|
1401
2468
|
def _run_for(params: dict) -> _Run | None:
|
|
1402
2469
|
stream_id = str(params.get("stream_id") or "").strip()
|
|
1403
2470
|
session_id = str(params.get("session_id") or "").strip()
|
|
@@ -1417,15 +2484,20 @@ def register(registry) -> None:
|
|
|
1417
2484
|
workspace = params.get("workspace")
|
|
1418
2485
|
if not isinstance(workspace, str) or not workspace.strip():
|
|
1419
2486
|
raise InvalidParams("workspace is required")
|
|
1420
|
-
with
|
|
2487
|
+
with agent_scope(profile_home_param(params)):
|
|
1421
2488
|
with _turn_identity(session_id, workspace):
|
|
1422
2489
|
return start(ctx, params)
|
|
1423
2490
|
|
|
1424
2491
|
@registry.method("chat.compress")
|
|
1425
2492
|
def compress_(ctx: CallContext, params: dict) -> dict:
|
|
1426
|
-
with
|
|
2493
|
+
with agent_scope(profile_home_param(params)):
|
|
1427
2494
|
return compress(ctx, params)
|
|
1428
2495
|
|
|
2496
|
+
@registry.method("chat.refine")
|
|
2497
|
+
def refine_(ctx: CallContext, params: dict) -> dict:
|
|
2498
|
+
with agent_scope(profile_home_param(params)):
|
|
2499
|
+
return {"output": refine(params)}
|
|
2500
|
+
|
|
1429
2501
|
@registry.method("chat.compress_finalize", requires_agent=False)
|
|
1430
2502
|
def compress_finalize_(ctx: CallContext, params: dict) -> dict:
|
|
1431
2503
|
token = str(params.get("commit_token") or "").strip()
|
|
@@ -1487,36 +2559,133 @@ def register(registry) -> None:
|
|
|
1487
2559
|
pending, index = _steer_queue_params(params)
|
|
1488
2560
|
run = _run_for(params)
|
|
1489
2561
|
if run is None or run.agent is None:
|
|
1490
|
-
return {"redirected": False, "withdrawn": False}
|
|
2562
|
+
return {"redirected": False, "withdrawn": False, "reason": "not_running"}
|
|
1491
2563
|
return steer_now(run.agent, pending, index)
|
|
1492
2564
|
|
|
2565
|
+
@registry.method("chat.finalize_session", requires_agent=False)
|
|
2566
|
+
def finalize_(ctx: CallContext, params: dict) -> dict:
|
|
2567
|
+
session_id = str(params.get("session_id") or "").strip()
|
|
2568
|
+
reason = params.get("reason")
|
|
2569
|
+
if not session_id or reason not in ("clear", "delete", "archive"):
|
|
2570
|
+
raise InvalidParams("session_id and reason (clear/delete/archive) are required")
|
|
2571
|
+
lineage = params.get("lineage")
|
|
2572
|
+
if lineage is not None and (not isinstance(lineage, list) or len(lineage) > 10_000
|
|
2573
|
+
or any(not isinstance(sid, str) or not sid for sid in lineage)):
|
|
2574
|
+
raise InvalidParams("lineage must contain session ids")
|
|
2575
|
+
home = profile_home_param(params)
|
|
2576
|
+
messages: list[str] = []
|
|
2577
|
+
|
|
2578
|
+
def finalize() -> None:
|
|
2579
|
+
try:
|
|
2580
|
+
# Scope inside the worker: a timed-out plugin keeps its own profile until it finishes.
|
|
2581
|
+
with agent_scope(home):
|
|
2582
|
+
if reason == "archive":
|
|
2583
|
+
_notify_archive(home, session_id, lineage)
|
|
2584
|
+
return
|
|
2585
|
+
from hermes_cli import lifecycle
|
|
2586
|
+
|
|
2587
|
+
callback = getattr(lifecycle, "finalize_session", None)
|
|
2588
|
+
if callback is None:
|
|
2589
|
+
return
|
|
2590
|
+
results = callback(session_id=session_id, platform="webui", reason=reason)
|
|
2591
|
+
extract = getattr(lifecycle, "session_end_messages", None)
|
|
2592
|
+
if extract is not None:
|
|
2593
|
+
messages.extend(extract(results))
|
|
2594
|
+
except Exception: # noqa: BLE001 - hooks must never block the boundary
|
|
2595
|
+
log.warning("Session lifecycle callback failed (%s)", reason)
|
|
2596
|
+
finally:
|
|
2597
|
+
with _RELEASES_LOCK:
|
|
2598
|
+
_RELEASES.pop(worker, None)
|
|
2599
|
+
|
|
2600
|
+
if reason == "archive":
|
|
2601
|
+
worker = _ArchiveCallback(finalize)
|
|
2602
|
+
_schedule_archive(worker, home)
|
|
2603
|
+
return {"messages": []}
|
|
2604
|
+
worker = threading.Thread(target=finalize, name="session-finalize", daemon=True)
|
|
2605
|
+
worker.failed = False # type: ignore[attr-defined] - hook failures do not prevent profile deletion
|
|
2606
|
+
with _RELEASES_LOCK:
|
|
2607
|
+
_RELEASES[worker] = home
|
|
2608
|
+
worker.start()
|
|
2609
|
+
worker.join(_SESSION_FINALIZE_TIMEOUT)
|
|
2610
|
+
if worker.is_alive():
|
|
2611
|
+
log.warning("Session lifecycle callback exceeded deadline (%s)", reason)
|
|
2612
|
+
return {"messages": []}
|
|
2613
|
+
return {"messages": messages}
|
|
2614
|
+
|
|
1493
2615
|
@registry.method("chat.evict_agent", requires_agent=False)
|
|
1494
2616
|
def evict_(ctx: CallContext, params: dict) -> dict:
|
|
1495
2617
|
"""Drop a session's cached agent. Only delete/clear (``clear_session``) end its approval grants, parked
|
|
1496
|
-
approvals and
|
|
2618
|
+
approvals, kernels and background review; a model switch, truncate or compress leaves a running turn's agent
|
|
2619
|
+
in place."""
|
|
1497
2620
|
session_id = str(params.get("session_id") or "").strip()
|
|
1498
2621
|
end_session = params.get("clear_session") is True
|
|
1499
2622
|
with _AGENT_CACHE_LOCK:
|
|
1500
2623
|
with _RUNS_LOCK:
|
|
1501
2624
|
run = _RUNS.get(_RUNS_BY_SESSION.get(session_id) or "")
|
|
2625
|
+
if end_session and run is not None:
|
|
2626
|
+
run.session_ended = True
|
|
1502
2627
|
if not end_session and run is not None and not run.finished.is_set():
|
|
1503
2628
|
return {"evicted": False}
|
|
1504
2629
|
entry = _AGENT_CACHE.pop(session_id, None)
|
|
1505
2630
|
_release_agents_locked([entry] if entry else [])
|
|
1506
2631
|
if end_session:
|
|
2632
|
+
# The server does not wait for an eviction; the cancels finish on their own.
|
|
2633
|
+
_cancel_background_reviews(([(entry[0], entry[2])] if entry else []) + ([(run.agent, run.home)] if run else []), "session deleted or cleared")
|
|
1507
2634
|
try:
|
|
1508
2635
|
from tools.approval import clear_session
|
|
1509
2636
|
clear_session(session_id)
|
|
1510
2637
|
except Exception: # noqa: BLE001
|
|
1511
2638
|
pass
|
|
2639
|
+
# TAL-887: a password manager this session unlocked locks again with it; other sessions keep theirs.
|
|
2640
|
+
_release_vault_session(session_id)
|
|
1512
2641
|
return {"evicted": entry is not None}
|
|
1513
2642
|
|
|
2643
|
+
@registry.method("chat.context_detail", requires_agent=False)
|
|
2644
|
+
def context_detail_(ctx: CallContext, params: dict) -> dict:
|
|
2645
|
+
from ..context_detail import project, unavailable
|
|
2646
|
+
home = profile_home_param(params).resolve()
|
|
2647
|
+
sid = str(params.get("session_id") or "")
|
|
2648
|
+
with _AGENT_CACHE_LOCK:
|
|
2649
|
+
cached = _AGENT_CACHE.get(sid)
|
|
2650
|
+
if cached is None:
|
|
2651
|
+
return unavailable("not_built")
|
|
2652
|
+
agent = cached[0]
|
|
2653
|
+
if (Path(cached[2]).resolve() != home or getattr(agent, "model", None) != params.get("model")
|
|
2654
|
+
or getattr(agent, "provider", None) != params.get("model_provider")):
|
|
2655
|
+
return unavailable("changed")
|
|
2656
|
+
def marks():
|
|
2657
|
+
comp = getattr(agent, "context_compressor", None)
|
|
2658
|
+
return (getattr(agent, "model", None), getattr(agent, "provider", None),
|
|
2659
|
+
getattr(comp, "compression_count", None), getattr(comp, "last_prompt_tokens", None),
|
|
2660
|
+
len(getattr(agent, "_session_messages", None) or []))
|
|
2661
|
+
before = marks()
|
|
2662
|
+
# Pin the cache entry across the read just like refine/memory, so eviction defers its release.
|
|
2663
|
+
_COMMITTING.append((agent, cached[2]))
|
|
2664
|
+
try:
|
|
2665
|
+
from agent.context_breakdown import compute_session_context_breakdown
|
|
2666
|
+
from agent.context_file_sources import context_file_sources_for_agent
|
|
2667
|
+
with scoped_home(home), session_workspace(params.get("workspace")):
|
|
2668
|
+
payload = compute_session_context_breakdown(agent, list(getattr(agent, "_session_messages", None) or []))
|
|
2669
|
+
payload['context_files'] = context_file_sources_for_agent(agent)
|
|
2670
|
+
with _AGENT_CACHE_LOCK:
|
|
2671
|
+
if _AGENT_CACHE.get(sid) is not cached or before != marks():
|
|
2672
|
+
return unavailable("changed")
|
|
2673
|
+
return project(payload)
|
|
2674
|
+
except ImportError:
|
|
2675
|
+
return unavailable("unsupported")
|
|
2676
|
+
except Exception: # never expose Agent diagnostics, prompt contents or paths
|
|
2677
|
+
return unavailable()
|
|
2678
|
+
finally:
|
|
2679
|
+
with _AGENT_CACHE_LOCK:
|
|
2680
|
+
_COMMITTING.remove((agent, cached[2]))
|
|
2681
|
+
_release_if_dropped_locked(agent, cached[2])
|
|
2682
|
+
|
|
1514
2683
|
@registry.method("chat.commit_memory", requires_agent=False)
|
|
1515
2684
|
def commit_memory_(ctx: CallContext, params: dict) -> dict:
|
|
1516
2685
|
"""Predecessor ``commit_session_memory``: flush the cached Agent's memory for a session the user just left.
|
|
1517
|
-
Providers get the session's transcript and run under
|
|
2686
|
+
Providers get the session's transcript and run under the ``scoped_home`` the Agent was built in, which v0.22.3
|
|
2687
|
+
binds them to (like ``_release_agent``), not the request's ``profile_home``."""
|
|
1518
2688
|
session_id = str(params.get("session_id") or "").strip()
|
|
1519
|
-
home = profile_home_param(params)
|
|
1520
2689
|
with _AGENT_CACHE_LOCK:
|
|
1521
2690
|
cached = _AGENT_CACHE.get(session_id)
|
|
1522
2691
|
with _RUNS_LOCK:
|
|
@@ -1525,16 +2694,16 @@ def register(registry) -> None:
|
|
|
1525
2694
|
commit = getattr(agent, "commit_memory_session", None) if agent is not None else None
|
|
1526
2695
|
if commit is None or busy:
|
|
1527
2696
|
return {"committed": False}
|
|
1528
|
-
_COMMITTING.append(agent)
|
|
2697
|
+
_COMMITTING.append((agent, cached[2]))
|
|
1529
2698
|
try:
|
|
1530
|
-
with scoped_home(
|
|
2699
|
+
with scoped_home(cached[2]):
|
|
1531
2700
|
commit(list(getattr(agent, "_session_messages", None) or []))
|
|
1532
2701
|
except Exception as exc: # noqa: BLE001
|
|
1533
2702
|
log.warning("commit_memory_session() failed for session %s: %s", session_id, exc)
|
|
1534
2703
|
return {"committed": False}
|
|
1535
2704
|
finally:
|
|
1536
2705
|
with _AGENT_CACHE_LOCK:
|
|
1537
|
-
_COMMITTING.remove(agent)
|
|
2706
|
+
_COMMITTING.remove((agent, cached[2]))
|
|
1538
2707
|
_release_if_dropped_locked(agent, cached[2])
|
|
1539
2708
|
return {"committed": True}
|
|
1540
2709
|
|
|
@@ -1551,7 +2720,7 @@ def register(registry) -> None:
|
|
|
1551
2720
|
raise RpcError(f"approval module unavailable: {exc}", condition="agent_unavailable") from exc
|
|
1552
2721
|
with scoped_home(profile_home_param(params)):
|
|
1553
2722
|
entries = approval_mod.list_gateway_approvals(session_id)
|
|
1554
|
-
targets = [e for e in entries if
|
|
2723
|
+
targets = [e for e in entries if e.get("request_id") == request_id] if request_id else entries[:1]
|
|
1555
2724
|
keys = []
|
|
1556
2725
|
for entry in targets:
|
|
1557
2726
|
keys.extend(entry.get("pattern_keys") or [entry.get("pattern_key", "")])
|
|
@@ -1559,17 +2728,21 @@ def register(registry) -> None:
|
|
|
1559
2728
|
if choice == "session":
|
|
1560
2729
|
for k in keys:
|
|
1561
2730
|
approval_mod.approve_session(session_id, k)
|
|
1562
|
-
elif choice == "always":
|
|
2731
|
+
elif choice == "always" and keys:
|
|
2732
|
+
permanent_set = getattr(approval_mod, "_permanent_set", None)
|
|
2733
|
+
baseline_key = getattr(approval_mod, "_baseline_key", None)
|
|
2734
|
+
baselines = getattr(approval_mod, "_permanent_baseline_by_home", None)
|
|
2735
|
+
if not callable(permanent_set) or not callable(baseline_key) or not isinstance(baselines, dict):
|
|
2736
|
+
raise RpcError("Agent cannot safely persist command approvals", condition="agent_incompatible")
|
|
1563
2737
|
for k in keys:
|
|
1564
2738
|
approval_mod.approve_session(session_id, k)
|
|
1565
2739
|
approval_mod.approve_permanent(k)
|
|
1566
2740
|
try:
|
|
1567
|
-
#
|
|
1568
|
-
#
|
|
1569
|
-
permanent_set = getattr(approval_mod, "_permanent_set", None)
|
|
2741
|
+
# Older retained Agents' _persist_choice saves full snapshots. Capture only unsaved grants
|
|
2742
|
+
# against this home's baseline, so a concurrent revocation cannot become fresh approval intent.
|
|
1570
2743
|
with approval_mod._lock:
|
|
1571
|
-
|
|
1572
|
-
approval_mod.save_permanent_allowlist(
|
|
2744
|
+
grants = permanent_set() - baselines.get(baseline_key(), set())
|
|
2745
|
+
approval_mod.save_permanent_allowlist(grants)
|
|
1573
2746
|
except Exception: # noqa: BLE001
|
|
1574
2747
|
log.debug("save_permanent_allowlist failed", exc_info=True)
|
|
1575
2748
|
resolved = approval_mod.resolve_gateway_approval(session_id, choice, request_id=request_id)
|
|
@@ -1600,22 +2773,27 @@ def register(registry) -> None:
|
|
|
1600
2773
|
|
|
1601
2774
|
@registry.method("clarify.respond", requires_agent=False)
|
|
1602
2775
|
def clarify_respond_(ctx: CallContext, params: dict) -> dict:
|
|
1603
|
-
response = str(params.get("response") or "")
|
|
2776
|
+
response = str(params.get("response") or "")
|
|
1604
2777
|
clarify_id = str(params.get("clarify_id") or "").strip()
|
|
1605
|
-
if not response:
|
|
1606
|
-
raise InvalidParams("response is required")
|
|
1607
2778
|
run = _run_for(params)
|
|
1608
2779
|
if run is None:
|
|
1609
2780
|
return {"ok": False}
|
|
2781
|
+
if params.get("session_id") and str(params["session_id"]) != run.session_id:
|
|
2782
|
+
return {"ok": False}
|
|
1610
2783
|
with run.lock:
|
|
1611
2784
|
entries = list(run.clarify_entries.values())
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2785
|
+
target = next((e for e in entries if e.clarify_id == clarify_id), None) if clarify_id else (entries[0] if entries else None)
|
|
2786
|
+
if target is None or target.event.is_set() or (target.deadline is not None and time.monotonic() >= target.deadline):
|
|
2787
|
+
return {"ok": False}
|
|
2788
|
+
skill_secret = target.data.get("kind") == "skill_secret"
|
|
2789
|
+
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):
|
|
2790
|
+
return {"ok": False}
|
|
2791
|
+
# Secure answers are as typed; an empty reply is their decline. Only the first answer releases a waiter.
|
|
2792
|
+
secure = str(target.data.get("kind") or "").startswith("vault_") or skill_secret
|
|
2793
|
+
if not secure:
|
|
2794
|
+
response = response.strip()
|
|
2795
|
+
if not response and not secure:
|
|
2796
|
+
raise InvalidParams("response is required")
|
|
2797
|
+
target.result = response
|
|
2798
|
+
target.event.set()
|
|
2799
|
+
return {"ok": True, "clarify_id": target.clarify_id}
|