@k2b/cloud 0.6.0-rc.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/README.md +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,1624 @@
|
|
|
1
|
+
import type { CompactEvent, NessiLoop, OutboundEvent, Provider, Tool, ToolResolver } from "@k2b/nessi";
|
|
2
|
+
import { compact, nessi } from "@k2b/nessi";
|
|
3
|
+
import { loadCurrentHelp } from "../_internal/help-catalog";
|
|
4
|
+
import { listCapabilities } from "../_internal/registry";
|
|
5
|
+
import type { CapabilityActionReview } from "../contracts/capabilities";
|
|
6
|
+
import type { AccessSubject, RequestActor } from "../server";
|
|
7
|
+
import { logger } from "../services/logging";
|
|
8
|
+
import { coreSettings } from "../services/settings/api";
|
|
9
|
+
import { normalizeLocale } from "../shared/locale";
|
|
10
|
+
import {
|
|
11
|
+
type AiToolApprovalContext,
|
|
12
|
+
aiToolAllowsAlways,
|
|
13
|
+
aiToolApprovalScope,
|
|
14
|
+
aiTurnAllowsRememberedApprovals,
|
|
15
|
+
hasRememberedAiToolApproval,
|
|
16
|
+
} from "./approvals";
|
|
17
|
+
import { isAssistantChatTurn } from "./assistant-models";
|
|
18
|
+
import { createAiToolResolver } from "./capabilities";
|
|
19
|
+
import { executeAiCapability, resolveAiCapabilityActor, reviewAiCapability } from "./capability-execution";
|
|
20
|
+
import { createCloudCompactFn } from "./compaction";
|
|
21
|
+
import { createCloudAiLocalBashTool, createConfiguredDefaultCloudAiTools } from "./default-tools";
|
|
22
|
+
import { aiFileStore } from "./files-store";
|
|
23
|
+
import { aiMemories } from "./memories";
|
|
24
|
+
import { createCloudAiMemoryTool } from "./memory-tool";
|
|
25
|
+
import { recordAiMemoryWorkflowEvidence } from "./memory-workflow-evidence";
|
|
26
|
+
import { aiModelAccess } from "./model-access";
|
|
27
|
+
import { type AiUserPrefs, aiActorUser, aiUserPrefs } from "./prefs";
|
|
28
|
+
import { createCloudAiReadProjectKnowledgeTool, createCloudAiSearchProjectTool } from "./project-tool";
|
|
29
|
+
import { aiProjects } from "./projects";
|
|
30
|
+
import {
|
|
31
|
+
type AiTurnBlock,
|
|
32
|
+
type AiWireEvent,
|
|
33
|
+
applyWireEventToBlocks,
|
|
34
|
+
buildBlocksFromMessages,
|
|
35
|
+
compactionBlockId,
|
|
36
|
+
reconcileResolvedTurnActions,
|
|
37
|
+
steerAppliedBlockId,
|
|
38
|
+
steerMessageBlockId,
|
|
39
|
+
streamBlockId,
|
|
40
|
+
toolBlockId,
|
|
41
|
+
} from "./protocol";
|
|
42
|
+
import { collectConversationResourceObservations } from "./resource-refs";
|
|
43
|
+
import { isAiVisionModelConfigured, type resolveAiModel } from "./settings";
|
|
44
|
+
import { selectAiSkillCatalog } from "./skill-catalog";
|
|
45
|
+
import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool } from "./skill-tool";
|
|
46
|
+
import { aiSkills } from "./skills";
|
|
47
|
+
import { aiConversations } from "./store";
|
|
48
|
+
import { publishAiWireEvent } from "./stream";
|
|
49
|
+
import { composeAiSystemPrompt } from "./system-prompt";
|
|
50
|
+
import { aiToolAudit } from "./tool-audit";
|
|
51
|
+
import { resolveAiToolResultMaxChars } from "./tool-result-budget";
|
|
52
|
+
import { aiToolPromptHints, type PreparedAiTools, prepareAiTools } from "./tools";
|
|
53
|
+
import type {
|
|
54
|
+
AiChatTurnRunConfig,
|
|
55
|
+
AiFrontendToolMode,
|
|
56
|
+
AiPendingTurnActionRecord,
|
|
57
|
+
AiRuntimeTool,
|
|
58
|
+
AiStoredMessage,
|
|
59
|
+
AiToolPresentation,
|
|
60
|
+
AiTurnClaim,
|
|
61
|
+
AiTurnFinalizedEvent,
|
|
62
|
+
AiTurnRunConfig,
|
|
63
|
+
AiTurnSteer,
|
|
64
|
+
} from "./types";
|
|
65
|
+
import { isAiImageMediaType } from "./types";
|
|
66
|
+
import { validateAiTurnRequest } from "./validate";
|
|
67
|
+
|
|
68
|
+
const log = logger("ai:executor");
|
|
69
|
+
|
|
70
|
+
const AI_TURN_LEASE_MS = 45_000;
|
|
71
|
+
const AI_COALESCE_MS = 25;
|
|
72
|
+
const AI_COALESCE_MAX_CHARS = 512;
|
|
73
|
+
const AI_SNAPSHOT_INTERVAL_MS = 1_000;
|
|
74
|
+
const AI_ACTION_BUDGET_MS = 24 * 60 * 60_000;
|
|
75
|
+
const AI_FINAL_TOOL_ROUND_PROMPT = `# Final response
|
|
76
|
+
The configured tool-round budget has been reached, so no more tools are available in this turn. Answer the user's request now with the best result supported by the evidence already gathered. State any material uncertainty or incomplete part clearly.`;
|
|
77
|
+
|
|
78
|
+
const toolRoundState = (messages: AiStoredMessage[]): { issued: number; completed: number } => {
|
|
79
|
+
const completedCallIds = new Set(messages.flatMap(({ message }) => (message.role === "tool_result" ? [message.callId] : [])));
|
|
80
|
+
const rounds = messages.flatMap(({ message }) => {
|
|
81
|
+
if (message.role !== "assistant") return [];
|
|
82
|
+
const callIds = message.content.flatMap((block) => (block.type === "tool_call" ? [block.id] : []));
|
|
83
|
+
return callIds.length > 0 ? [callIds] : [];
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
issued: rounds.length,
|
|
87
|
+
completed: rounds.filter((callIds) => callIds.every((callId) => completedCallIds.has(callId))).length,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const applyToolRoundPolicy = (input: {
|
|
92
|
+
provider: Provider;
|
|
93
|
+
tools: Tool[] | ToolResolver;
|
|
94
|
+
maxToolRounds?: number;
|
|
95
|
+
issuedToolRounds: number;
|
|
96
|
+
completedToolRounds: number;
|
|
97
|
+
}): { provider: Provider; tools: Tool[] | ToolResolver; maxTurns?: number; noteToolRound: () => void } => {
|
|
98
|
+
const limit = Math.floor(input.maxToolRounds ?? 0);
|
|
99
|
+
if (limit <= 0) return { provider: input.provider, tools: input.tools, noteToolRound: () => undefined };
|
|
100
|
+
|
|
101
|
+
const issuedAtStart = Math.max(0, Math.floor(input.issuedToolRounds));
|
|
102
|
+
let completed = Math.max(0, Math.floor(input.completedToolRounds));
|
|
103
|
+
let finalSynthesis = completed >= limit;
|
|
104
|
+
const tools: ToolResolver = async () => {
|
|
105
|
+
finalSynthesis = completed >= limit;
|
|
106
|
+
if (finalSynthesis) return [];
|
|
107
|
+
return typeof input.tools === "function" ? input.tools() : input.tools;
|
|
108
|
+
};
|
|
109
|
+
const provider: Provider = {
|
|
110
|
+
name: input.provider.name,
|
|
111
|
+
family: input.provider.family,
|
|
112
|
+
model: input.provider.model,
|
|
113
|
+
contextWindow: input.provider.contextWindow,
|
|
114
|
+
capabilities: input.provider.capabilities,
|
|
115
|
+
complete: (request) => input.provider.complete(request),
|
|
116
|
+
stream: async function* (request) {
|
|
117
|
+
yield* input.provider.stream(
|
|
118
|
+
finalSynthesis ? { ...request, systemPrompt: `${request.systemPrompt ?? ""}\n\n${AI_FINAL_TOOL_ROUND_PROMPT}`.trim() } : request,
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Nessi checks this before provider calls. The extra round is the tool-free
|
|
124
|
+
// synthesis call after the last allowed tool-using round.
|
|
125
|
+
return {
|
|
126
|
+
provider,
|
|
127
|
+
tools,
|
|
128
|
+
maxTurns: Math.max(1, limit - issuedAtStart + 1),
|
|
129
|
+
noteToolRound: () => {
|
|
130
|
+
completed += 1;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const indexConversationResources = async (input: Parameters<typeof aiConversations.indexConversationResources>[0]): Promise<void> => {
|
|
136
|
+
try {
|
|
137
|
+
await aiConversations.indexConversationResources(input);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
log.warn("Failed to index AI conversation resources", {
|
|
140
|
+
conversationId: input.conversationId,
|
|
141
|
+
turnId: input.turnId,
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const recordMemoryWorkflowEvidence = async (input: Parameters<typeof recordAiMemoryWorkflowEvidence>[0]): Promise<void> => {
|
|
148
|
+
try {
|
|
149
|
+
await recordAiMemoryWorkflowEvidence(input);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.warn("Failed to record AI workflow evidence", {
|
|
152
|
+
conversationId: input.conversationId,
|
|
153
|
+
turnId: input.turnId,
|
|
154
|
+
capabilityId: input.capabilityId,
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const indexConversationToolSource = async (input: {
|
|
161
|
+
conversationId: string;
|
|
162
|
+
turnId: string;
|
|
163
|
+
callId: string;
|
|
164
|
+
name: string;
|
|
165
|
+
args: unknown;
|
|
166
|
+
result: unknown;
|
|
167
|
+
isError: boolean;
|
|
168
|
+
}): Promise<void> => {
|
|
169
|
+
if (input.isError) return;
|
|
170
|
+
try {
|
|
171
|
+
let source: Parameters<typeof aiConversations.indexConversationSource>[0]["source"] | null = null;
|
|
172
|
+
if (input.name === "web_search") {
|
|
173
|
+
const args = input.args;
|
|
174
|
+
const query = typeof args === "object" && args !== null && "query" in args && typeof args.query === "string" ? args.query.trim() : "";
|
|
175
|
+
source = { kind: "activity", key: "web_search", title: query || "Web search", preview: "Searched the web", icon: "ti ti-world" };
|
|
176
|
+
} else if (input.name === "web_extract" && typeof input.result === "object" && input.result !== null) {
|
|
177
|
+
const result = input.result as Record<string, unknown>;
|
|
178
|
+
if (typeof result.url === "string" && result.url.trim()) {
|
|
179
|
+
const url = new URL(result.url);
|
|
180
|
+
url.hash = "";
|
|
181
|
+
source = {
|
|
182
|
+
kind: "web",
|
|
183
|
+
key: url.href,
|
|
184
|
+
title: typeof result.title === "string" && result.title.trim() ? result.title.trim() : url.hostname,
|
|
185
|
+
preview: typeof result.description === "string" ? result.description.trim().slice(0, 500) : undefined,
|
|
186
|
+
icon: "ti ti-world",
|
|
187
|
+
href: url.href,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!source) return;
|
|
192
|
+
await aiConversations.indexConversationSource({
|
|
193
|
+
conversationId: input.conversationId,
|
|
194
|
+
turnId: input.turnId,
|
|
195
|
+
callId: input.callId,
|
|
196
|
+
source,
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
log.warn("Failed to index AI conversation source", {
|
|
200
|
+
conversationId: input.conversationId,
|
|
201
|
+
turnId: input.turnId,
|
|
202
|
+
error: error instanceof Error ? error.message : String(error),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const memoryQueryFromInput = (input: unknown): string => {
|
|
208
|
+
if (typeof input === "string") return input;
|
|
209
|
+
if (!Array.isArray(input)) return "";
|
|
210
|
+
return input
|
|
211
|
+
.map((part) => {
|
|
212
|
+
if (typeof part === "string") return part;
|
|
213
|
+
if (typeof part !== "object" || part === null || !("type" in part)) return "";
|
|
214
|
+
return part.type === "text" && "text" in part && typeof part.text === "string" ? part.text : "";
|
|
215
|
+
})
|
|
216
|
+
.join(" ")
|
|
217
|
+
.trim();
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const accessSubjectForActor = (actor: RequestActor | undefined): AccessSubject | null => {
|
|
221
|
+
if (!actor) return null;
|
|
222
|
+
if (actor.kind === "user") return { type: "user", userId: actor.user.id };
|
|
223
|
+
if (actor.delegatedUser) {
|
|
224
|
+
return { type: "user", userId: actor.delegatedUser.id, delegatedByServiceAccountId: actor.serviceAccount.id };
|
|
225
|
+
}
|
|
226
|
+
return { type: "service_account", serviceAccountId: actor.serviceAccount.id };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export type ExecutorConfig = {
|
|
230
|
+
leaseOwner: string;
|
|
231
|
+
heartbeatMs: number;
|
|
232
|
+
/** Re-enqueue continuation work after a suspension so any worker can resume it. */
|
|
233
|
+
enqueueContinuation: (input: { conversationId: string; turnId: string }) => Promise<void>;
|
|
234
|
+
/** Settings/model resolution seam — tests inject a fake so they never touch shared settings. */
|
|
235
|
+
validateTurn?: typeof validateAiTurnRequest;
|
|
236
|
+
/** Runs after the durable turn state and final wire event are flushed. */
|
|
237
|
+
onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
type ResolvedModel = Awaited<ReturnType<typeof resolveAiModel>>;
|
|
241
|
+
type ValidatedTurn = { settings: Awaited<ReturnType<typeof validateAiTurnRequest>>["settings"]; resolved: ResolvedModel };
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Baseline rebuild — reconstruct the full active-turn view from persisted rounds
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
const rebuildBlocksFromMessages = (
|
|
248
|
+
messages: AiStoredMessage[],
|
|
249
|
+
pending: AiPendingTurnActionRecord[],
|
|
250
|
+
steers: AiTurnSteer[] = [],
|
|
251
|
+
): AiTurnBlock[] => {
|
|
252
|
+
const blocks = buildBlocksFromMessages(messages);
|
|
253
|
+
const indexByCallId = new Map(blocks.map((block, index) => [block.kind === "tool" ? block.callId : `_${index}`, index]));
|
|
254
|
+
|
|
255
|
+
for (const action of pending) {
|
|
256
|
+
const parentCallId = action.kind === "custom_approval" ? customApprovalParentCallId(action.callId) : undefined;
|
|
257
|
+
const at = indexByCallId.get(action.callId) ?? (parentCallId ? indexByCallId.get(parentCallId) : undefined);
|
|
258
|
+
const existing = at !== undefined ? blocks[at] : undefined;
|
|
259
|
+
if (existing?.kind === "tool") {
|
|
260
|
+
blocks[at!] = {
|
|
261
|
+
...existing,
|
|
262
|
+
callId: action.callId,
|
|
263
|
+
status: action.kind === "client_tool" ? "awaiting_client" : "awaiting_approval",
|
|
264
|
+
approval:
|
|
265
|
+
action.kind === "client_tool" ? undefined : { message: action.message, review: action.review, allowAlways: action.allowAlways },
|
|
266
|
+
frontendMode: action.frontendMode,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const known = new Set(blocks.map((block) => block.id));
|
|
272
|
+
for (const steer of steers) {
|
|
273
|
+
if (steer.status === "discarded" || known.has(steerMessageBlockId(steer.id))) continue;
|
|
274
|
+
blocks.push({
|
|
275
|
+
id: steerMessageBlockId(steer.id),
|
|
276
|
+
kind: "steer_message",
|
|
277
|
+
steerId: steer.id,
|
|
278
|
+
text: steer.text,
|
|
279
|
+
status: steer.status === "pending" ? "pending" : "consumed",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return blocks;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
type ChatAttemptState = {
|
|
287
|
+
loopMessages: AiStoredMessage[];
|
|
288
|
+
pendingRecords: AiPendingTurnActionRecord[];
|
|
289
|
+
resolvedRecords: AiPendingTurnActionRecord[];
|
|
290
|
+
turnSteers: AiTurnSteer[];
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const rebuildAttemptBaseline = (state: ChatAttemptState): AiTurnBlock[] => {
|
|
294
|
+
const persisted = rebuildBlocksFromMessages(state.loopMessages, state.pendingRecords, state.turnSteers);
|
|
295
|
+
const unresolvedCallIds = new Set(
|
|
296
|
+
persisted.flatMap((block) => (block.kind === "tool" && block.status === "running" ? [block.callId] : [])),
|
|
297
|
+
);
|
|
298
|
+
const unreflectedResolvedRecords = state.resolvedRecords.filter((action) => {
|
|
299
|
+
if (unresolvedCallIds.has(action.callId)) return true;
|
|
300
|
+
const parentCallId = action.kind === "custom_approval" ? customApprovalParentCallId(action.callId) : undefined;
|
|
301
|
+
return parentCallId ? unresolvedCallIds.has(parentCallId) : false;
|
|
302
|
+
});
|
|
303
|
+
if (unreflectedResolvedRecords.length === 0) return persisted;
|
|
304
|
+
return reconcileResolvedTurnActions(
|
|
305
|
+
rebuildBlocksFromMessages(state.loopMessages, [...state.pendingRecords, ...unreflectedResolvedRecords], state.turnSteers),
|
|
306
|
+
unreflectedResolvedRecords,
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const loadChatAttemptState = async (conversationId: string, turnId: string): Promise<ChatAttemptState> => {
|
|
311
|
+
const [loopMessages, pendingRecords, resolvedRecords, turnSteers] = await Promise.all([
|
|
312
|
+
aiConversations.listTurnMessages({ conversationId, loopId: turnId }),
|
|
313
|
+
aiConversations.listPendingActionRecords({ conversationId, turnId }),
|
|
314
|
+
aiConversations.listResolvedPendingActions({ conversationId, turnId }),
|
|
315
|
+
aiConversations.listTurnSteers({ conversationId, turnId }),
|
|
316
|
+
]);
|
|
317
|
+
return { loopMessages, pendingRecords, resolvedRecords, turnSteers };
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Event mapper — Nessi block/tool events to Cloud wire block ops
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
type BlockSetOp = { type: "block_set"; block: AiTurnBlock };
|
|
325
|
+
type BlockDeltaOp = { type: "block_delta"; blockId: string; blockKind: "text" | "thinking"; delta: string };
|
|
326
|
+
type BlockOp = BlockSetOp | BlockDeltaOp;
|
|
327
|
+
|
|
328
|
+
type ToolBlockPatch = Partial<Extract<AiTurnBlock, { kind: "tool" }>> & { name?: string; clearApproval?: boolean };
|
|
329
|
+
|
|
330
|
+
const customApprovalParentCallId = (callId: string): string | undefined => /^(.*)-approval-\d+$/.exec(callId)?.[1];
|
|
331
|
+
|
|
332
|
+
const approvalReviewForCallId = (
|
|
333
|
+
reviews: ReadonlyMap<string, CapabilityActionReview>,
|
|
334
|
+
callId: string,
|
|
335
|
+
): CapabilityActionReview | undefined => {
|
|
336
|
+
const direct = reviews.get(callId);
|
|
337
|
+
if (direct) return direct;
|
|
338
|
+
const parentCallId = customApprovalParentCallId(callId);
|
|
339
|
+
return parentCallId ? reviews.get(parentCallId) : undefined;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Map nessi's canonical block/tool events to Cloud wire ops. nessi owns block
|
|
344
|
+
* structure and whitespace hygiene; the mapper only (a) scopes stream block ids
|
|
345
|
+
* to (attempt, turn) so re-claimed attempts never collide, and (b) maintains
|
|
346
|
+
* tool blocks keyed by callId, enriched with Cloud status/approval metadata.
|
|
347
|
+
*/
|
|
348
|
+
const createEventMapper = (attempt: number, seedBlocks: AiTurnBlock[], allowRememberedApprovals = true) => {
|
|
349
|
+
const toolBlocks = new Map<string, Extract<AiTurnBlock, { kind: "tool" }>>();
|
|
350
|
+
for (const block of seedBlocks) {
|
|
351
|
+
if (block.kind === "tool") toolBlocks.set(block.callId, block);
|
|
352
|
+
}
|
|
353
|
+
/** Real frontend mode per tool name — set once the turn's tools are prepared.
|
|
354
|
+
* Getting this wrong is not cosmetic: the client auto-resolves plain "client"
|
|
355
|
+
* blocks, so a mislabeled client_interaction tool (survey) would be answered
|
|
356
|
+
* with a fake result before the user ever sees it. */
|
|
357
|
+
let frontendModes = new Map<string, AiFrontendToolMode>();
|
|
358
|
+
const setFrontendModes = (modes: Map<string, AiFrontendToolMode>) => {
|
|
359
|
+
frontendModes = modes;
|
|
360
|
+
};
|
|
361
|
+
let presentations = new Map<string, AiToolPresentation>();
|
|
362
|
+
const setPresentations = (items: Map<string, AiToolPresentation>) => {
|
|
363
|
+
presentations = items;
|
|
364
|
+
};
|
|
365
|
+
let canonicalNames = new Map<string, string>();
|
|
366
|
+
const setCanonicalNames = (items: Map<string, string>) => {
|
|
367
|
+
canonicalNames = items;
|
|
368
|
+
};
|
|
369
|
+
let approvalReviews = new Map<string, CapabilityActionReview>();
|
|
370
|
+
const setApprovalReviews = (items: Map<string, CapabilityActionReview>) => {
|
|
371
|
+
approvalReviews = items;
|
|
372
|
+
};
|
|
373
|
+
let approvalPolicies: PreparedAiTools["approvalPolicies"] = new Map();
|
|
374
|
+
const setApprovalPolicies = (items: PreparedAiTools["approvalPolicies"]) => {
|
|
375
|
+
approvalPolicies = items;
|
|
376
|
+
};
|
|
377
|
+
let rejectedCallIds = new Set<string>();
|
|
378
|
+
const setRejectedCallIds = (items: Set<string>) => {
|
|
379
|
+
rejectedCallIds = items;
|
|
380
|
+
};
|
|
381
|
+
/** nessi stream block ids (turn-scoped) that belong to tool_call blocks — their deltas are raw args JSON. */
|
|
382
|
+
const toolStreamIds = new Set<string>();
|
|
383
|
+
/** kind per open Cloud stream block id, for delta create-if-missing. */
|
|
384
|
+
const streamKinds = new Map<string, "text" | "thinking">();
|
|
385
|
+
|
|
386
|
+
const setTool = (callId: string, patch: ToolBlockPatch, displayCallId = callId): BlockOp => {
|
|
387
|
+
const existing = toolBlocks.get(callId);
|
|
388
|
+
const rawName = patch.name ?? existing?.name ?? "tool";
|
|
389
|
+
const name = canonicalNames.get(rawName) ?? rawName;
|
|
390
|
+
const approval = patch.clearApproval ? undefined : "approval" in patch ? patch.approval : existing?.approval;
|
|
391
|
+
const block: Extract<AiTurnBlock, { kind: "tool" }> = {
|
|
392
|
+
id: toolBlockId(displayCallId),
|
|
393
|
+
kind: "tool",
|
|
394
|
+
callId,
|
|
395
|
+
name,
|
|
396
|
+
args: "args" in patch ? patch.args : existing?.args,
|
|
397
|
+
status: patch.status ?? existing?.status ?? "running",
|
|
398
|
+
result: "result" in patch ? patch.result : existing?.result,
|
|
399
|
+
isError: "isError" in patch ? patch.isError : existing?.isError,
|
|
400
|
+
approval: approval && !allowRememberedApprovals ? { ...approval, allowAlways: false } : approval,
|
|
401
|
+
frontendMode: patch.frontendMode ?? existing?.frontendMode,
|
|
402
|
+
presentation: patch.presentation ?? existing?.presentation ?? presentations.get(rawName) ?? presentations.get(name),
|
|
403
|
+
};
|
|
404
|
+
toolBlocks.set(callId, block);
|
|
405
|
+
return { type: "block_set", block };
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const compaction = (
|
|
409
|
+
status: "running" | "completed" | "failed",
|
|
410
|
+
result?: Extract<AiTurnBlock, { kind: "compaction" }>["result"],
|
|
411
|
+
): BlockOp => ({
|
|
412
|
+
type: "block_set",
|
|
413
|
+
block: { id: compactionBlockId, kind: "compaction", status, ...(result ? { result } : {}) },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const translate = (event: OutboundEvent): BlockOp[] => {
|
|
417
|
+
switch (event.type) {
|
|
418
|
+
case "block_start": {
|
|
419
|
+
if (event.kind === "tool_call") {
|
|
420
|
+
toolStreamIds.add(`${event.turnIndex}:${event.blockId}`);
|
|
421
|
+
if (!event.callId) return [];
|
|
422
|
+
return [setTool(event.callId, { name: event.name, status: "running" })];
|
|
423
|
+
}
|
|
424
|
+
const id = streamBlockId(attempt, event.turnIndex, event.blockId);
|
|
425
|
+
streamKinds.set(id, event.kind);
|
|
426
|
+
return [{ type: "block_set", block: { id, kind: event.kind, text: "" } }];
|
|
427
|
+
}
|
|
428
|
+
case "block_delta": {
|
|
429
|
+
if (toolStreamIds.has(`${event.turnIndex}:${event.blockId}`)) return []; // raw args JSON — not rendered
|
|
430
|
+
const id = streamBlockId(attempt, event.turnIndex, event.blockId);
|
|
431
|
+
return [{ type: "block_delta", blockId: id, blockKind: streamKinds.get(id) ?? "text", delta: event.delta }];
|
|
432
|
+
}
|
|
433
|
+
case "block_end": {
|
|
434
|
+
if (event.block.type === "tool_call") {
|
|
435
|
+
return [setTool(event.block.id, { name: event.block.name, args: event.block.args, status: "running" })];
|
|
436
|
+
}
|
|
437
|
+
// Converge on the final block content (covers any missed delta).
|
|
438
|
+
const id = streamBlockId(attempt, event.turnIndex, event.blockId);
|
|
439
|
+
const text = event.block.type === "text" ? event.block.text : event.block.thinking;
|
|
440
|
+
return [{ type: "block_set", block: { id, kind: event.block.type, text } }];
|
|
441
|
+
}
|
|
442
|
+
case "tool_execution_start":
|
|
443
|
+
if (rejectedCallIds.has(event.callId)) return [];
|
|
444
|
+
return [setTool(event.callId, { name: event.name, args: event.args, status: "running" })];
|
|
445
|
+
case "tool_action_request":
|
|
446
|
+
const displayCallId = event.kind === "custom_approval" ? (customApprovalParentCallId(event.callId) ?? event.callId) : event.callId;
|
|
447
|
+
const review = approvalReviewForCallId(approvalReviews, event.callId);
|
|
448
|
+
return [
|
|
449
|
+
setTool(
|
|
450
|
+
event.callId,
|
|
451
|
+
{
|
|
452
|
+
name: event.name,
|
|
453
|
+
args: event.args,
|
|
454
|
+
status: event.kind === "client_tool" ? "awaiting_client" : "awaiting_approval",
|
|
455
|
+
approval:
|
|
456
|
+
event.kind === "client_tool"
|
|
457
|
+
? undefined
|
|
458
|
+
: {
|
|
459
|
+
message: event.message,
|
|
460
|
+
review,
|
|
461
|
+
allowAlways: review?.approvalScope !== undefined || aiToolAllowsAlways(approvalPolicies.get(event.name)),
|
|
462
|
+
},
|
|
463
|
+
frontendMode: event.kind === "client_tool" ? (frontendModes.get(event.name) ?? "client") : undefined,
|
|
464
|
+
},
|
|
465
|
+
displayCallId,
|
|
466
|
+
),
|
|
467
|
+
];
|
|
468
|
+
case "tool_execution_end":
|
|
469
|
+
return [
|
|
470
|
+
setTool(event.callId, {
|
|
471
|
+
name: event.name,
|
|
472
|
+
status: rejectedCallIds.has(event.callId) ? "rejected" : event.isError ? "failed" : "completed",
|
|
473
|
+
result: event.result,
|
|
474
|
+
isError: Boolean(event.isError),
|
|
475
|
+
clearApproval: true,
|
|
476
|
+
}),
|
|
477
|
+
];
|
|
478
|
+
case "issue": {
|
|
479
|
+
const callId = "callId" in event.issue ? event.issue.callId : undefined;
|
|
480
|
+
if (callId && rejectedCallIds.has(callId)) return [];
|
|
481
|
+
if (callId && toolBlocks.has(callId)) {
|
|
482
|
+
const existing = toolBlocks.get(callId);
|
|
483
|
+
if (existing && existing.status !== "completed" && existing.status !== "failed") {
|
|
484
|
+
return [setTool(callId, { status: "failed", result: event.issue.message, isError: true, clearApproval: true })];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
case "compaction_start":
|
|
490
|
+
return [compaction("running")];
|
|
491
|
+
case "compaction_end":
|
|
492
|
+
return [compaction("completed")];
|
|
493
|
+
default:
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
translate,
|
|
500
|
+
compaction,
|
|
501
|
+
setFrontendModes,
|
|
502
|
+
setPresentations,
|
|
503
|
+
setCanonicalNames,
|
|
504
|
+
setApprovalPolicies,
|
|
505
|
+
setApprovalReviews,
|
|
506
|
+
setRejectedCallIds,
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Run-config materialization
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
type MaterializedChatConfig = {
|
|
515
|
+
actor?: RequestActor;
|
|
516
|
+
systemPrompt?: string;
|
|
517
|
+
tools: AiRuntimeTool[];
|
|
518
|
+
toolApprovalContext?: AiToolApprovalContext;
|
|
519
|
+
modelPolicy: AiChatTurnRunConfig["modelPolicy"];
|
|
520
|
+
requestedModelId?: string;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const materializeChatConfig = async (config: AiChatTurnRunConfig, signal: AbortSignal, turnId: string): Promise<MaterializedChatConfig> => {
|
|
524
|
+
const source = config.toolSource ?? { kind: "none" };
|
|
525
|
+
return {
|
|
526
|
+
actor: config.actor,
|
|
527
|
+
systemPrompt: config.systemPrompt,
|
|
528
|
+
tools:
|
|
529
|
+
source.kind === "default"
|
|
530
|
+
? [
|
|
531
|
+
...(await createConfiguredDefaultCloudAiTools()),
|
|
532
|
+
...(config.clientToolIds?.includes("local_bash") ? [createCloudAiLocalBashTool()] : []),
|
|
533
|
+
]
|
|
534
|
+
: [],
|
|
535
|
+
toolApprovalContext: config.toolApprovalContext,
|
|
536
|
+
modelPolicy: config.modelPolicy,
|
|
537
|
+
requestedModelId: config.requestedModelId,
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
// Executor
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
type AttemptOutcome = { kind: "finished"; status: "completed" | "failed" | "aborted"; error: string | null } | { kind: "suspended" };
|
|
546
|
+
|
|
547
|
+
export class AiTurnExecutor {
|
|
548
|
+
constructor(private readonly config: ExecutorConfig) {}
|
|
549
|
+
|
|
550
|
+
async run(input: { conversationId: string; turnId: string; claim: AiTurnClaim; signal: AbortSignal }): Promise<void> {
|
|
551
|
+
const { conversationId, turnId, claim, signal } = input;
|
|
552
|
+
const pipeline = new StreamPipeline({
|
|
553
|
+
conversationId,
|
|
554
|
+
turnId,
|
|
555
|
+
attempt: claim.turn.attempt,
|
|
556
|
+
startSeq: claim.liveSeq,
|
|
557
|
+
leaseOwner: this.config.leaseOwner,
|
|
558
|
+
seedBlocks: claim.liveBlocks ?? [],
|
|
559
|
+
allowRememberedApprovals: aiTurnAllowsRememberedApprovals(claim.runConfig),
|
|
560
|
+
});
|
|
561
|
+
const runConfig = claim.runConfig;
|
|
562
|
+
if (!runConfig) {
|
|
563
|
+
pipeline.seedBaseline(claim.liveBlocks ?? []);
|
|
564
|
+
await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
|
|
565
|
+
await pipeline.emitBaseline();
|
|
566
|
+
await this.finalize(conversationId, turnId, pipeline, "failed", "AI turn is missing its run configuration.", null);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (runConfig.kind === "compact") {
|
|
571
|
+
pipeline.seedBaseline(claim.liveBlocks ?? []);
|
|
572
|
+
await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
|
|
573
|
+
await pipeline.emitBaseline();
|
|
574
|
+
await this.runCompaction(conversationId, turnId, pipeline, runConfig, signal);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let attemptState: ChatAttemptState;
|
|
579
|
+
try {
|
|
580
|
+
attemptState = await loadChatAttemptState(conversationId, turnId);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
pipeline.seedBaseline(claim.liveBlocks ?? []);
|
|
583
|
+
await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
|
|
584
|
+
await pipeline.emitBaseline();
|
|
585
|
+
await this.finalize(
|
|
586
|
+
conversationId,
|
|
587
|
+
turnId,
|
|
588
|
+
pipeline,
|
|
589
|
+
"failed",
|
|
590
|
+
error instanceof Error ? error.message : "AI turn state could not be loaded.",
|
|
591
|
+
"chat",
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
pipeline.seedBaseline(rebuildAttemptBaseline(attemptState));
|
|
596
|
+
await pipeline.emitTurnStarted(claim.turn.modelProfileId ?? "");
|
|
597
|
+
await pipeline.emitBaseline();
|
|
598
|
+
await this.runChat(conversationId, turnId, claim, runConfig, pipeline, signal, false, attemptState);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private async finalize(
|
|
602
|
+
conversationId: string,
|
|
603
|
+
turnId: string,
|
|
604
|
+
pipeline: StreamPipeline,
|
|
605
|
+
status: "completed" | "failed" | "aborted",
|
|
606
|
+
error: string | null,
|
|
607
|
+
kind: AiTurnRunConfig["kind"] | null,
|
|
608
|
+
) {
|
|
609
|
+
if (status === "failed" && error) log.error("AI turn failed", { conversationId, turnId, error });
|
|
610
|
+
const finalized = await aiConversations.completeTurn({ conversationId, turnId, status, error, leaseOwner: this.config.leaseOwner });
|
|
611
|
+
if (finalized === "completed") await pipeline.emitTurnFinished(status, error);
|
|
612
|
+
await pipeline.flush().catch(() => undefined);
|
|
613
|
+
if (finalized === "completed" && this.config.onTurnFinalized) {
|
|
614
|
+
await this.config.onTurnFinalized({ conversationId, turnId, status, kind }).catch((hookError) => {
|
|
615
|
+
log.warn("AI turn finalized hook failed", {
|
|
616
|
+
conversationId,
|
|
617
|
+
turnId,
|
|
618
|
+
status,
|
|
619
|
+
error: hookError instanceof Error ? hookError.message : "AI turn finalized hook failed",
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return finalized;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private async runChat(
|
|
627
|
+
conversationId: string,
|
|
628
|
+
turnId: string,
|
|
629
|
+
claim: AiTurnClaim,
|
|
630
|
+
config: AiChatTurnRunConfig,
|
|
631
|
+
pipeline: StreamPipeline,
|
|
632
|
+
signal: AbortSignal,
|
|
633
|
+
skipResolvedActions = false,
|
|
634
|
+
attemptState?: ChatAttemptState,
|
|
635
|
+
): Promise<void> {
|
|
636
|
+
const startedAt = Date.now();
|
|
637
|
+
const abortController = new AbortController();
|
|
638
|
+
const onSignal = () => abortController.abort();
|
|
639
|
+
if (signal.aborted) abortController.abort();
|
|
640
|
+
else signal.addEventListener("abort", onSignal, { once: true });
|
|
641
|
+
|
|
642
|
+
let material: MaterializedChatConfig;
|
|
643
|
+
let validated: ValidatedTurn;
|
|
644
|
+
let resolvedProjectId: string | null = null;
|
|
645
|
+
let chatId = config.chatId ?? "";
|
|
646
|
+
let allowedTools: string[] | null = null;
|
|
647
|
+
try {
|
|
648
|
+
const [nextMaterial, conversation] = await Promise.all([
|
|
649
|
+
materializeChatConfig(config, abortController.signal, turnId),
|
|
650
|
+
aiConversations.getConversation({ conversationId }),
|
|
651
|
+
]);
|
|
652
|
+
material = nextMaterial;
|
|
653
|
+
if (!conversation) throw new Error("Conversation is no longer available.");
|
|
654
|
+
allowedTools = conversation.allowedTools ?? null;
|
|
655
|
+
const allowed = allowedTools === null ? null : new Set(allowedTools);
|
|
656
|
+
if (allowed) material.tools = material.tools.filter((tool) => allowed.has(tool.def.name));
|
|
657
|
+
chatId ||= conversation?.shortId ?? "";
|
|
658
|
+
if (config.project) {
|
|
659
|
+
const subject = accessSubjectForActor(material.actor);
|
|
660
|
+
const project = subject ? await aiProjects.getByShortId(config.project.id, subject, "read") : null;
|
|
661
|
+
if (!project) {
|
|
662
|
+
throw new Error("Project access is no longer available.");
|
|
663
|
+
}
|
|
664
|
+
resolvedProjectId = project.id;
|
|
665
|
+
}
|
|
666
|
+
if (isAssistantChatTurn(config) && claim.turn.modelProfileId) {
|
|
667
|
+
await aiModelAccess.assertAllowed(claim.turn.modelProfileId, accessSubjectForActor(material.actor));
|
|
668
|
+
}
|
|
669
|
+
validated = await (this.config.validateTurn ?? validateAiTurnRequest)({
|
|
670
|
+
input: config.input,
|
|
671
|
+
hasImageAttachments: config.files?.attached.some((file) => isAiImageMediaType(file.mediaType)),
|
|
672
|
+
canInspectAttachedImages:
|
|
673
|
+
material.tools.some((tool) => tool.def.name === "view_image") &&
|
|
674
|
+
(await isAiVisionModelConfigured(material.modelPolicy?.allowedDataBoundaries)),
|
|
675
|
+
modelPolicy: material.modelPolicy,
|
|
676
|
+
requestedModelId: isAssistantChatTurn(config)
|
|
677
|
+
? (claim.turn.modelProfileId ?? material.requestedModelId)
|
|
678
|
+
: material.requestedModelId,
|
|
679
|
+
});
|
|
680
|
+
if (isAssistantChatTurn(config)) {
|
|
681
|
+
await aiModelAccess.assertAllowed(validated.resolved.profile.id, accessSubjectForActor(material.actor));
|
|
682
|
+
}
|
|
683
|
+
} catch (error) {
|
|
684
|
+
signal.removeEventListener("abort", onSignal);
|
|
685
|
+
await this.finalize(conversationId, turnId, pipeline, "failed", error instanceof Error ? error.message : "AI turn failed", "chat");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const { settings, resolved } = validated;
|
|
689
|
+
|
|
690
|
+
const defaultToolSource = config.toolSource?.kind === "default" ? config.toolSource : null;
|
|
691
|
+
const toolActor =
|
|
692
|
+
defaultToolSource && resolved.profile.capabilities.includes("tools") && material.actor?.kind === "user" ? material.actor : null;
|
|
693
|
+
const helpEnabled = toolActor !== null;
|
|
694
|
+
const toolDiscoveryEnabled = toolActor !== null;
|
|
695
|
+
const appToolsEnabled = toolActor !== null && defaultToolSource?.appTools === true;
|
|
696
|
+
let capabilityAuthority: Awaited<ReturnType<typeof resolveAiCapabilityActor>> | null = null;
|
|
697
|
+
try {
|
|
698
|
+
capabilityAuthority = appToolsEnabled
|
|
699
|
+
? await resolveAiCapabilityActor({ conversationId, persistedActor: material.actor, store: aiConversations })
|
|
700
|
+
: null;
|
|
701
|
+
} catch (error) {
|
|
702
|
+
signal.removeEventListener("abort", onSignal);
|
|
703
|
+
await this.finalize(
|
|
704
|
+
conversationId,
|
|
705
|
+
turnId,
|
|
706
|
+
pipeline,
|
|
707
|
+
"failed",
|
|
708
|
+
error instanceof Error ? error.message : "Cloud capability actor resolution failed",
|
|
709
|
+
"chat",
|
|
710
|
+
);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (capabilityAuthority) material.actor = capabilityAuthority.actor;
|
|
714
|
+
|
|
715
|
+
// Personalization applies to user-backed personal conversations.
|
|
716
|
+
const user = aiActorUser(material.actor);
|
|
717
|
+
let prefs: AiUserPrefs | null = null;
|
|
718
|
+
if (user && config.toolSource?.kind === "default") {
|
|
719
|
+
prefs = await aiUserPrefs.get(user.id);
|
|
720
|
+
}
|
|
721
|
+
const query = memoryQueryFromInput(config.input);
|
|
722
|
+
const memoryActive = Boolean(prefs?.memoryEnabled);
|
|
723
|
+
const memory = memoryActive && user ? await aiMemories.selectHot(user.id, query) : null;
|
|
724
|
+
const timeZone = String((await coreSettings.get<string>("app.timezone")) || "").trim() || "UTC";
|
|
725
|
+
const promptLocale = normalizeLocale(config.locale ?? (await coreSettings.get<string>("app.locale")));
|
|
726
|
+
const project = config.project;
|
|
727
|
+
const projectSubject = project ? accessSubjectForActor(material.actor) : null;
|
|
728
|
+
// Skills are an Assistant/default-tool capability. Custom and structured
|
|
729
|
+
// executions must not gain an implicit database dependency or extra tools.
|
|
730
|
+
const skillSubject =
|
|
731
|
+
defaultToolSource && resolved.profile.capabilities.includes("tools") ? accessSubjectForActor(material.actor) : null;
|
|
732
|
+
const availableSkills = skillSubject ? (await aiSkills.list(skillSubject)).filter((skill) => skill.enabled) : [];
|
|
733
|
+
const skillCatalog = selectAiSkillCatalog(availableSkills, resolved.provider.contextWindow ?? 0, query);
|
|
734
|
+
const projectFiles =
|
|
735
|
+
project && resolvedProjectId && projectSubject
|
|
736
|
+
? {
|
|
737
|
+
list: async () =>
|
|
738
|
+
(await aiProjects.listFiles(resolvedProjectId, projectSubject)).map((file) => ({
|
|
739
|
+
path: file.path,
|
|
740
|
+
mediaType: file.mediaType,
|
|
741
|
+
size: file.size,
|
|
742
|
+
updatedAt: file.updatedAt,
|
|
743
|
+
})),
|
|
744
|
+
read: async (path: string) => {
|
|
745
|
+
const file = await aiProjects.readFileByPath(resolvedProjectId, path, projectSubject);
|
|
746
|
+
return file
|
|
747
|
+
? { path: file.path, mediaType: file.mediaType, size: file.size, updatedAt: file.updatedAt, bytes: file.bytes }
|
|
748
|
+
: null;
|
|
749
|
+
},
|
|
750
|
+
}
|
|
751
|
+
: undefined;
|
|
752
|
+
const skillFiles = skillSubject
|
|
753
|
+
? {
|
|
754
|
+
list: () => aiSkills.listTurnFiles(turnId, skillSubject),
|
|
755
|
+
read: (path: string) => aiSkills.readTurnFile(turnId, path, skillSubject),
|
|
756
|
+
}
|
|
757
|
+
: undefined;
|
|
758
|
+
const runtimeTools = [
|
|
759
|
+
...material.tools,
|
|
760
|
+
...(memoryActive ? [createCloudAiMemoryTool(query)] : []),
|
|
761
|
+
...(skillSubject && availableSkills.length ? [createCloudAiLoadSkillTool(skillSubject)] : []),
|
|
762
|
+
...(skillSubject && skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(skillSubject)] : []),
|
|
763
|
+
...(config.project && resolvedProjectId && projectSubject
|
|
764
|
+
? [
|
|
765
|
+
createCloudAiSearchProjectTool(resolvedProjectId, projectSubject),
|
|
766
|
+
createCloudAiReadProjectKnowledgeTool(resolvedProjectId, projectSubject),
|
|
767
|
+
]
|
|
768
|
+
: []),
|
|
769
|
+
];
|
|
770
|
+
const toolsSupported = resolved.profile.capabilities.includes("tools");
|
|
771
|
+
const allowed = allowedTools === null ? null : new Set(allowedTools);
|
|
772
|
+
const activeTools = toolsSupported ? runtimeTools.filter((tool) => !allowed || allowed.has(tool.def.name)) : [];
|
|
773
|
+
const memoryToolEnabled = activeTools.some((tool) => tool.def.name === "memory");
|
|
774
|
+
const projectToolEnabled = activeTools.some((tool) => tool.def.name === "search_project");
|
|
775
|
+
|
|
776
|
+
if (config.project?.references.length) {
|
|
777
|
+
await indexConversationResources({
|
|
778
|
+
conversationId,
|
|
779
|
+
turnId,
|
|
780
|
+
resources: config.project.references.map((ref) => ({ ref })),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const dynamicToolRuntimeContext = {
|
|
785
|
+
turnId,
|
|
786
|
+
attachedFilePaths: new Set(config.files?.attached.map((file) => file.path) ?? []),
|
|
787
|
+
allowedDataBoundaries: material.modelPolicy?.allowedDataBoundaries,
|
|
788
|
+
projectFiles,
|
|
789
|
+
skillFiles,
|
|
790
|
+
selectedModel: resolved,
|
|
791
|
+
locale: promptLocale,
|
|
792
|
+
timeZone,
|
|
793
|
+
};
|
|
794
|
+
const prepared = prepareAiTools({
|
|
795
|
+
tools: activeTools,
|
|
796
|
+
...dynamicToolRuntimeContext,
|
|
797
|
+
actor: material.actor,
|
|
798
|
+
conversationId,
|
|
799
|
+
});
|
|
800
|
+
const rememberableCapabilityApprovals = new Map<string, string>();
|
|
801
|
+
const capabilityActionReviews = new Map<string, CapabilityActionReview>();
|
|
802
|
+
pipeline.setFrontendModes(prepared.frontendModes);
|
|
803
|
+
pipeline.setCanonicalNames(prepared.canonicalNames);
|
|
804
|
+
pipeline.setApprovalPolicies(prepared.approvalPolicies);
|
|
805
|
+
const toolPresentations = new Map<string, AiToolPresentation>();
|
|
806
|
+
const rejectedToolCallIds = new Set<string>();
|
|
807
|
+
pipeline.setPresentations(toolPresentations);
|
|
808
|
+
pipeline.setApprovalReviews(capabilityActionReviews);
|
|
809
|
+
let turnInput = config.input;
|
|
810
|
+
try {
|
|
811
|
+
if (resolved.profile.capabilities.includes("vision") && config.files?.attached.some((file) => isAiImageMediaType(file.mediaType))) {
|
|
812
|
+
const parts = typeof turnInput === "string" ? [{ type: "text" as const, text: turnInput }] : [...turnInput];
|
|
813
|
+
for (const file of config.files.attached) {
|
|
814
|
+
if (!isAiImageMediaType(file.mediaType)) continue;
|
|
815
|
+
const stored = await aiFileStore.readTurnFile({ turnId, path: file.path });
|
|
816
|
+
if (!stored) throw new Error(`Attached conversation image is no longer available: ${file.path}`);
|
|
817
|
+
parts.push({ type: "file", mediaType: stored.mediaType, data: Buffer.from(stored.bytes).toString("base64") });
|
|
818
|
+
}
|
|
819
|
+
turnInput = parts;
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
signal.removeEventListener("abort", onSignal);
|
|
823
|
+
await this.finalize(
|
|
824
|
+
conversationId,
|
|
825
|
+
turnId,
|
|
826
|
+
pipeline,
|
|
827
|
+
"failed",
|
|
828
|
+
error instanceof Error ? error.message : "Image preparation failed",
|
|
829
|
+
"chat",
|
|
830
|
+
);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const store = aiConversations.createSessionStore({
|
|
834
|
+
conversationId,
|
|
835
|
+
modelProfileId: resolved.profile.id,
|
|
836
|
+
turnId,
|
|
837
|
+
leaseOwner: this.config.leaseOwner,
|
|
838
|
+
turnInput,
|
|
839
|
+
toolPresentations,
|
|
840
|
+
rejectedToolCallIds,
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const { loopMessages, pendingRecords, resolvedRecords, turnSteers } =
|
|
844
|
+
attemptState ?? (await loadChatAttemptState(conversationId, turnId));
|
|
845
|
+
for (const action of resolvedRecords) {
|
|
846
|
+
if (action.resolvedEvent?.type !== "approval_response" || action.resolvedEvent.approved) continue;
|
|
847
|
+
rejectedToolCallIds.add(
|
|
848
|
+
action.kind === "custom_approval" ? (customApprovalParentCallId(action.callId) ?? action.callId) : action.callId,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
pipeline.setRejectedCallIds(rejectedToolCallIds);
|
|
852
|
+
const assistantMessages = loopMessages.filter((message) => message.message.role !== "user");
|
|
853
|
+
const isFresh = assistantMessages.length === 0 && resolvedRecords.length === 0 && !skipResolvedActions;
|
|
854
|
+
|
|
855
|
+
// Rebuild the whole active-turn view so a re-run/continuation reconstructs it.
|
|
856
|
+
if (!attemptState) {
|
|
857
|
+
pipeline.seedBaseline(rebuildAttemptBaseline({ loopMessages, pendingRecords, resolvedRecords, turnSteers }));
|
|
858
|
+
await pipeline.emitBaseline();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const appliedSteers: AiTurnSteer[] = [];
|
|
862
|
+
|
|
863
|
+
const tools = toolActor
|
|
864
|
+
? createAiToolResolver({
|
|
865
|
+
conversationId,
|
|
866
|
+
actor: capabilityAuthority?.actor ?? toolActor,
|
|
867
|
+
staticTools: activeTools,
|
|
868
|
+
allowedTools,
|
|
869
|
+
runtimeContext: dynamicToolRuntimeContext,
|
|
870
|
+
store: aiConversations,
|
|
871
|
+
...(capabilityAuthority ? { listRegistry: listCapabilities } : {}),
|
|
872
|
+
onCapabilityRegistryError: (error) =>
|
|
873
|
+
log.warn("AI Capability registry unavailable; continuing without app capabilities", {
|
|
874
|
+
error: error instanceof Error ? error.message : String(error),
|
|
875
|
+
}),
|
|
876
|
+
listHelpRegistry: loadCurrentHelp,
|
|
877
|
+
locale: promptLocale,
|
|
878
|
+
onHelpRegistryError: (error) =>
|
|
879
|
+
log.warn("AI Help registry unavailable; continuing without Help documents", {
|
|
880
|
+
error: error instanceof Error ? error.message : String(error),
|
|
881
|
+
}),
|
|
882
|
+
maxLoadedTools: resolved.profile.maxLoadedTools,
|
|
883
|
+
...(capabilityAuthority
|
|
884
|
+
? {
|
|
885
|
+
review: (entry, args, context) =>
|
|
886
|
+
reviewAiCapability({
|
|
887
|
+
conversationId,
|
|
888
|
+
authority: capabilityAuthority!,
|
|
889
|
+
mandate: config.mandate,
|
|
890
|
+
locale: promptLocale,
|
|
891
|
+
entry,
|
|
892
|
+
args,
|
|
893
|
+
context,
|
|
894
|
+
}),
|
|
895
|
+
}
|
|
896
|
+
: {}),
|
|
897
|
+
onReview: (callId, review) => {
|
|
898
|
+
capabilityActionReviews.set(callId, review);
|
|
899
|
+
if (review.approvalScope) rememberableCapabilityApprovals.set(callId, review.approvalScope);
|
|
900
|
+
},
|
|
901
|
+
...(capabilityAuthority
|
|
902
|
+
? {
|
|
903
|
+
execute: async (entry, args, context) => {
|
|
904
|
+
try {
|
|
905
|
+
const result = await executeAiCapability({
|
|
906
|
+
conversationId,
|
|
907
|
+
turnId,
|
|
908
|
+
authority: capabilityAuthority!,
|
|
909
|
+
mandate: config.mandate,
|
|
910
|
+
actionApproval: entry.kind === "action" ? "approved" : undefined,
|
|
911
|
+
locale: promptLocale,
|
|
912
|
+
entry,
|
|
913
|
+
args,
|
|
914
|
+
context,
|
|
915
|
+
});
|
|
916
|
+
const resources = collectConversationResourceObservations(args, result);
|
|
917
|
+
if (resources.length) {
|
|
918
|
+
await Promise.all([
|
|
919
|
+
indexConversationResources({ conversationId, turnId, callId: context.callId, resources }),
|
|
920
|
+
recordMemoryWorkflowEvidence({
|
|
921
|
+
userId: capabilityAuthority.actor.user.id,
|
|
922
|
+
conversationId,
|
|
923
|
+
turnId,
|
|
924
|
+
capabilityId: entry.name,
|
|
925
|
+
resources,
|
|
926
|
+
}),
|
|
927
|
+
]);
|
|
928
|
+
}
|
|
929
|
+
return result;
|
|
930
|
+
} catch (error) {
|
|
931
|
+
const resources = collectConversationResourceObservations(args);
|
|
932
|
+
if (resources.length) {
|
|
933
|
+
await indexConversationResources({ conversationId, turnId, callId: context.callId, resources });
|
|
934
|
+
}
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
}
|
|
939
|
+
: {}),
|
|
940
|
+
onPrepared: ({ prepared: snapshot, presentations, rememberableApprovals }) => {
|
|
941
|
+
prepared.canonicalNames.clear();
|
|
942
|
+
prepared.approvalPolicies.clear();
|
|
943
|
+
prepared.frontendModes.clear();
|
|
944
|
+
rememberableCapabilityApprovals.clear();
|
|
945
|
+
toolPresentations.clear();
|
|
946
|
+
for (const [name, canonicalName] of snapshot.canonicalNames) prepared.canonicalNames.set(name, canonicalName);
|
|
947
|
+
for (const [name, policy] of snapshot.approvalPolicies) prepared.approvalPolicies.set(name, policy);
|
|
948
|
+
for (const [name, mode] of snapshot.frontendModes) prepared.frontendModes.set(name, mode);
|
|
949
|
+
for (const [name, scope] of rememberableApprovals) rememberableCapabilityApprovals.set(name, scope);
|
|
950
|
+
for (const [name, presentation] of presentations) toolPresentations.set(name, presentation);
|
|
951
|
+
pipeline.setFrontendModes(prepared.frontendModes);
|
|
952
|
+
pipeline.setCanonicalNames(prepared.canonicalNames);
|
|
953
|
+
pipeline.setApprovalPolicies(prepared.approvalPolicies);
|
|
954
|
+
pipeline.setPresentations(toolPresentations);
|
|
955
|
+
},
|
|
956
|
+
})
|
|
957
|
+
: prepared.tools;
|
|
958
|
+
|
|
959
|
+
const systemPrompt = composeAiSystemPrompt({
|
|
960
|
+
globalInstructions: settings.globalInstructions,
|
|
961
|
+
turnInstructions: [
|
|
962
|
+
material.systemPrompt,
|
|
963
|
+
...(allowedTools === null
|
|
964
|
+
? []
|
|
965
|
+
: [
|
|
966
|
+
`This conversation has a fixed tool scope. Only these task tools may be used: ${allowedTools.join(", ") || "none"}. Discovery cannot widen it. Explain unavailable operations; do not bypass this scope through another conversation or tool.`,
|
|
967
|
+
]),
|
|
968
|
+
]
|
|
969
|
+
.filter(Boolean)
|
|
970
|
+
.join("\n\n"),
|
|
971
|
+
chatId,
|
|
972
|
+
project: config.project,
|
|
973
|
+
files: config.files,
|
|
974
|
+
projectToolEnabled,
|
|
975
|
+
skills: activeTools.some((tool) => tool.def.name === "load_skill") ? skillCatalog.skills : undefined,
|
|
976
|
+
omittedSkillCount: activeTools.some((tool) => tool.def.name === "search_skills") ? skillCatalog.omitted : 0,
|
|
977
|
+
user,
|
|
978
|
+
memoryEnabled: memoryActive,
|
|
979
|
+
memoryToolEnabled,
|
|
980
|
+
helpEnabled,
|
|
981
|
+
toolDiscoveryEnabled,
|
|
982
|
+
appToolsEnabled,
|
|
983
|
+
toolHints: aiToolPromptHints(activeTools),
|
|
984
|
+
memory: memory?.text,
|
|
985
|
+
timeZone,
|
|
986
|
+
locale: promptLocale,
|
|
987
|
+
});
|
|
988
|
+
const priorToolRounds = toolRoundState(loopMessages);
|
|
989
|
+
const toolRoundPolicy = applyToolRoundPolicy({
|
|
990
|
+
provider: resolved.provider,
|
|
991
|
+
tools,
|
|
992
|
+
maxToolRounds: resolved.profile.maxToolRounds,
|
|
993
|
+
issuedToolRounds: priorToolRounds.issued,
|
|
994
|
+
completedToolRounds: priorToolRounds.completed,
|
|
995
|
+
});
|
|
996
|
+
const loop = nessi({
|
|
997
|
+
agentId: "cloud",
|
|
998
|
+
loopId: turnId,
|
|
999
|
+
...(isFresh ? { input: turnInput } : {}),
|
|
1000
|
+
provider: toolRoundPolicy.provider,
|
|
1001
|
+
systemPrompt,
|
|
1002
|
+
store,
|
|
1003
|
+
steering: async ({ signal: steeringSignal }) => {
|
|
1004
|
+
if (steeringSignal.aborted) return undefined;
|
|
1005
|
+
const steers = await aiConversations.takePendingTurnSteers({
|
|
1006
|
+
conversationId,
|
|
1007
|
+
turnId,
|
|
1008
|
+
leaseOwner: this.config.leaseOwner,
|
|
1009
|
+
});
|
|
1010
|
+
appliedSteers.push(...steers);
|
|
1011
|
+
return steers.length > 0 ? steers.map((steer) => steer.text) : undefined;
|
|
1012
|
+
},
|
|
1013
|
+
tools: toolRoundPolicy.tools,
|
|
1014
|
+
...(toolRoundPolicy.maxTurns === undefined ? {} : { maxTurns: toolRoundPolicy.maxTurns }),
|
|
1015
|
+
temperature: resolved.profile.temperature,
|
|
1016
|
+
maxOutputTokens: resolved.profile.maxOutputTokens,
|
|
1017
|
+
coalesce: { ms: AI_COALESCE_MS, maxChars: AI_COALESCE_MAX_CHARS },
|
|
1018
|
+
compact: createCloudCompactFn({
|
|
1019
|
+
conversationId,
|
|
1020
|
+
turnId,
|
|
1021
|
+
modelProfileId: resolved.profile.id,
|
|
1022
|
+
additionalInstructions: settings.compactionInstructions,
|
|
1023
|
+
maxOutputTokens: resolved.profile.maxOutputTokens,
|
|
1024
|
+
signal: abortController.signal,
|
|
1025
|
+
}),
|
|
1026
|
+
maxToolResultChars: resolveAiToolResultMaxChars({
|
|
1027
|
+
contextWindow: resolved.provider.contextWindow,
|
|
1028
|
+
configuredMaxChars: settings.maxToolResultChars,
|
|
1029
|
+
}),
|
|
1030
|
+
signal: abortController.signal,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// Seed the resumed loop with resolved actions before iterating.
|
|
1034
|
+
for (const record of skipResolvedActions ? [] : resolvedRecords) {
|
|
1035
|
+
if (record.resolvedEvent) loop.push(record.resolvedEvent);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const outcome = await this.driveChatLoop({
|
|
1039
|
+
loop,
|
|
1040
|
+
pipeline,
|
|
1041
|
+
conversationId,
|
|
1042
|
+
turnId,
|
|
1043
|
+
abortController,
|
|
1044
|
+
prepared,
|
|
1045
|
+
approvalContext: material.toolApprovalContext,
|
|
1046
|
+
allowRememberedApprovals: aiTurnAllowsRememberedApprovals(config),
|
|
1047
|
+
rememberableCapabilityApprovals,
|
|
1048
|
+
capabilityActionReviews,
|
|
1049
|
+
appliedSteers,
|
|
1050
|
+
noteToolRound: toolRoundPolicy.noteToolRound,
|
|
1051
|
+
});
|
|
1052
|
+
signal.removeEventListener("abort", onSignal);
|
|
1053
|
+
|
|
1054
|
+
if (outcome.kind === "suspended") {
|
|
1055
|
+
log.info("AI turn suspended", { conversationId, turnId, attempt: claim.turn.attempt, durationMs: Date.now() - startedAt });
|
|
1056
|
+
await this.config.enqueueContinuation({ conversationId, turnId }).catch(() => undefined);
|
|
1057
|
+
await pipeline.flush().catch(() => undefined);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const finalized = await this.finalize(conversationId, turnId, pipeline, outcome.status, outcome.error, "chat");
|
|
1062
|
+
if (finalized === "pending_steering" && outcome.status === "completed" && !signal.aborted) {
|
|
1063
|
+
await this.runChat(conversationId, turnId, claim, config, pipeline, signal, true);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
log.info("AI turn finished", {
|
|
1067
|
+
conversationId,
|
|
1068
|
+
turnId,
|
|
1069
|
+
attempt: claim.turn.attempt,
|
|
1070
|
+
status: outcome.status,
|
|
1071
|
+
durationMs: Date.now() - startedAt,
|
|
1072
|
+
firstBlockMs: pipeline.firstBlockMs,
|
|
1073
|
+
wireSeq: pipeline.seq,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
private async driveChatLoop(input: {
|
|
1078
|
+
loop: NessiLoop;
|
|
1079
|
+
pipeline: StreamPipeline;
|
|
1080
|
+
conversationId: string;
|
|
1081
|
+
turnId: string;
|
|
1082
|
+
abortController: AbortController;
|
|
1083
|
+
prepared: PreparedAiTools;
|
|
1084
|
+
approvalContext?: AiToolApprovalContext;
|
|
1085
|
+
allowRememberedApprovals: boolean;
|
|
1086
|
+
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1087
|
+
capabilityActionReviews: ReadonlyMap<string, CapabilityActionReview>;
|
|
1088
|
+
appliedSteers: AiTurnSteer[];
|
|
1089
|
+
noteToolRound: () => void;
|
|
1090
|
+
}): Promise<AttemptOutcome> {
|
|
1091
|
+
const {
|
|
1092
|
+
loop,
|
|
1093
|
+
pipeline,
|
|
1094
|
+
conversationId,
|
|
1095
|
+
turnId,
|
|
1096
|
+
abortController,
|
|
1097
|
+
prepared,
|
|
1098
|
+
approvalContext,
|
|
1099
|
+
allowRememberedApprovals,
|
|
1100
|
+
rememberableCapabilityApprovals,
|
|
1101
|
+
capabilityActionReviews,
|
|
1102
|
+
appliedSteers,
|
|
1103
|
+
noteToolRound,
|
|
1104
|
+
} = input;
|
|
1105
|
+
const stopHeartbeat = this.startHeartbeat(conversationId, turnId, abortController);
|
|
1106
|
+
let lastIssueMessage: string | null = null;
|
|
1107
|
+
|
|
1108
|
+
try {
|
|
1109
|
+
for await (const event of loop) {
|
|
1110
|
+
if (event.type === "tool_action_request") {
|
|
1111
|
+
const suspended = await this.handleActionRequest({
|
|
1112
|
+
event,
|
|
1113
|
+
loop,
|
|
1114
|
+
pipeline,
|
|
1115
|
+
conversationId,
|
|
1116
|
+
turnId,
|
|
1117
|
+
prepared,
|
|
1118
|
+
approvalContext,
|
|
1119
|
+
allowRememberedApprovals,
|
|
1120
|
+
rememberableCapabilityApprovals,
|
|
1121
|
+
capabilityActionReviews,
|
|
1122
|
+
});
|
|
1123
|
+
if (suspended) {
|
|
1124
|
+
abortController.abort();
|
|
1125
|
+
loop.abort();
|
|
1126
|
+
return { kind: "suspended" };
|
|
1127
|
+
}
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (event.type === "steer_applied") {
|
|
1132
|
+
const steer = appliedSteers.shift();
|
|
1133
|
+
if (steer) await pipeline.applySteer(steer);
|
|
1134
|
+
} else {
|
|
1135
|
+
await pipeline.apply(event);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (event.type === "tool_execution_start") {
|
|
1139
|
+
const toolName = prepared.canonicalNames.get(event.name) ?? event.name;
|
|
1140
|
+
await aiToolAudit
|
|
1141
|
+
.noteToolCall({
|
|
1142
|
+
conversationId,
|
|
1143
|
+
turnId,
|
|
1144
|
+
callId: event.callId,
|
|
1145
|
+
toolName,
|
|
1146
|
+
location: prepared.frontendModes.get(event.name) ?? "server",
|
|
1147
|
+
args: event.args,
|
|
1148
|
+
})
|
|
1149
|
+
.catch(() => undefined);
|
|
1150
|
+
} else if (event.type === "tool_execution_end") {
|
|
1151
|
+
await aiToolAudit
|
|
1152
|
+
.noteToolCompleted({ turnId, callId: event.callId, result: event.result, isError: event.isError })
|
|
1153
|
+
.catch(() => undefined);
|
|
1154
|
+
const toolBlock = pipeline.blocks.find((block) => block.kind === "tool" && block.callId === event.callId);
|
|
1155
|
+
await indexConversationToolSource({
|
|
1156
|
+
conversationId,
|
|
1157
|
+
turnId,
|
|
1158
|
+
callId: event.callId,
|
|
1159
|
+
name: event.name,
|
|
1160
|
+
args: toolBlock?.kind === "tool" ? toolBlock.args : undefined,
|
|
1161
|
+
result: event.result,
|
|
1162
|
+
isError: event.isError === true,
|
|
1163
|
+
});
|
|
1164
|
+
} else if (event.type === "turn_end" && event.message.content.some((block) => block.type === "tool_call")) {
|
|
1165
|
+
noteToolRound();
|
|
1166
|
+
} else if (event.type === "issue") {
|
|
1167
|
+
lastIssueMessage = event.issue.message;
|
|
1168
|
+
log.warn("AI turn issue", { conversationId, turnId, kind: event.issue.kind, message: event.issue.message });
|
|
1169
|
+
} else if (event.type === "loop_end") {
|
|
1170
|
+
const aggregate = event.aggregate;
|
|
1171
|
+
if (aggregate.assistantMessageCount > 0) {
|
|
1172
|
+
await aiConversations
|
|
1173
|
+
.setLatestAssistantLoopAggregate({ conversationId, loopId: turnId, aggregate, doneReason: event.reason })
|
|
1174
|
+
.catch(() => undefined);
|
|
1175
|
+
}
|
|
1176
|
+
if (event.reason === "aborted") return { kind: "finished", status: "aborted", error: null };
|
|
1177
|
+
if (event.reason === "stop") return { kind: "finished", status: "completed", error: null };
|
|
1178
|
+
if (event.reason === "max_turns") {
|
|
1179
|
+
return { kind: "finished", status: "failed", error: "The model did not produce a final answer within its tool-round limit." };
|
|
1180
|
+
}
|
|
1181
|
+
return { kind: "finished", status: "failed", error: lastIssueMessage ?? `AI turn ended: ${event.reason}` };
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return { kind: "finished", status: abortController.signal.aborted ? "aborted" : "completed", error: null };
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
if (abortController.signal.aborted) return { kind: "finished", status: "aborted", error: null };
|
|
1187
|
+
const message = error instanceof Error ? error.message : "AI turn failed";
|
|
1188
|
+
await pipeline.emitError(message).catch(() => undefined);
|
|
1189
|
+
return { kind: "finished", status: "failed", error: message };
|
|
1190
|
+
} finally {
|
|
1191
|
+
stopHeartbeat();
|
|
1192
|
+
await pipeline.flush().catch(() => undefined);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** Returns true when the turn was suspended for the action; false when resolved inline. */
|
|
1197
|
+
private async handleActionRequest(input: {
|
|
1198
|
+
event: Extract<OutboundEvent, { type: "tool_action_request" }>;
|
|
1199
|
+
loop: NessiLoop;
|
|
1200
|
+
pipeline: StreamPipeline;
|
|
1201
|
+
conversationId: string;
|
|
1202
|
+
turnId: string;
|
|
1203
|
+
prepared: PreparedAiTools;
|
|
1204
|
+
approvalContext?: AiToolApprovalContext;
|
|
1205
|
+
allowRememberedApprovals: boolean;
|
|
1206
|
+
rememberableCapabilityApprovals: ReadonlyMap<string, string>;
|
|
1207
|
+
capabilityActionReviews: ReadonlyMap<string, CapabilityActionReview>;
|
|
1208
|
+
}): Promise<boolean> {
|
|
1209
|
+
const {
|
|
1210
|
+
event,
|
|
1211
|
+
loop,
|
|
1212
|
+
pipeline,
|
|
1213
|
+
conversationId,
|
|
1214
|
+
turnId,
|
|
1215
|
+
prepared,
|
|
1216
|
+
approvalContext,
|
|
1217
|
+
allowRememberedApprovals,
|
|
1218
|
+
rememberableCapabilityApprovals,
|
|
1219
|
+
capabilityActionReviews,
|
|
1220
|
+
} = input;
|
|
1221
|
+
const approvalPolicy = prepared.approvalPolicies.get(event.name);
|
|
1222
|
+
const toolName = prepared.canonicalNames.get(event.name) ?? event.name;
|
|
1223
|
+
const frontendMode: AiFrontendToolMode | undefined =
|
|
1224
|
+
event.kind === "client_tool" ? (prepared.frontendModes.get(event.name) ?? "client") : undefined;
|
|
1225
|
+
const capabilityApprovalScope =
|
|
1226
|
+
event.kind === "custom_approval"
|
|
1227
|
+
? rememberableCapabilityApprovals.get(customApprovalParentCallId(event.callId) ?? event.callId)
|
|
1228
|
+
: undefined;
|
|
1229
|
+
const approvalScope = capabilityApprovalScope ?? aiToolApprovalScope(toolName, approvalPolicy);
|
|
1230
|
+
const allowAlways = allowRememberedApprovals && (capabilityApprovalScope !== undefined || aiToolAllowsAlways(approvalPolicy));
|
|
1231
|
+
|
|
1232
|
+
// Display-only client_view tools (e.g. cards) never need user input — resolve
|
|
1233
|
+
// inline and keep streaming instead of taking a full suspend/continuation trip.
|
|
1234
|
+
if (frontendMode === "client_view") {
|
|
1235
|
+
await pipeline.apply(event);
|
|
1236
|
+
loop.push({ type: "tool_result", callId: event.callId, result: { displayed: true } });
|
|
1237
|
+
// Mark the block completed immediately: nessi emits no tool_execution_end
|
|
1238
|
+
// for client tools, and a block stuck in awaiting_client would look like
|
|
1239
|
+
// an open action request if the turn suspends for another tool later.
|
|
1240
|
+
await pipeline.apply({
|
|
1241
|
+
type: "tool_execution_end",
|
|
1242
|
+
callId: event.callId,
|
|
1243
|
+
name: event.name,
|
|
1244
|
+
result: { displayed: true },
|
|
1245
|
+
isError: false,
|
|
1246
|
+
} as OutboundEvent);
|
|
1247
|
+
await aiToolAudit
|
|
1248
|
+
.noteToolCompleted({ turnId, callId: event.callId, result: { displayed: true }, isError: false })
|
|
1249
|
+
.catch(() => undefined);
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Remembered approvals resolve inline too.
|
|
1254
|
+
if (event.kind !== "client_tool" && allowAlways && approvalContext) {
|
|
1255
|
+
const remembered = await hasRememberedAiToolApproval(approvalContext, { toolName, approvalScope }).catch(() => false);
|
|
1256
|
+
if (remembered) {
|
|
1257
|
+
await aiToolAudit
|
|
1258
|
+
.noteApprovalResolved({ turnId, callId: event.callId, approvalState: "approved_by_preference" })
|
|
1259
|
+
.catch(() => undefined);
|
|
1260
|
+
loop.push({ type: "approval_response", callId: event.callId, approved: true });
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
await aiConversations.savePendingTurnAction({
|
|
1266
|
+
turnId,
|
|
1267
|
+
conversationId,
|
|
1268
|
+
callId: event.callId,
|
|
1269
|
+
kind: event.kind,
|
|
1270
|
+
status: "pending",
|
|
1271
|
+
name: toolName,
|
|
1272
|
+
args: event.args,
|
|
1273
|
+
message: event.message,
|
|
1274
|
+
review: event.kind === "custom_approval" ? approvalReviewForCallId(capabilityActionReviews, event.callId) : undefined,
|
|
1275
|
+
approvalScope,
|
|
1276
|
+
allowAlways,
|
|
1277
|
+
frontendMode,
|
|
1278
|
+
resolvedEvent: null,
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
if (event.kind === "client_tool") {
|
|
1282
|
+
await aiToolAudit
|
|
1283
|
+
.noteToolCall({
|
|
1284
|
+
conversationId,
|
|
1285
|
+
turnId,
|
|
1286
|
+
callId: event.callId,
|
|
1287
|
+
toolName,
|
|
1288
|
+
location: frontendMode ?? "client",
|
|
1289
|
+
args: event.args,
|
|
1290
|
+
status: "waiting_for_frontend",
|
|
1291
|
+
})
|
|
1292
|
+
.catch(() => undefined);
|
|
1293
|
+
} else {
|
|
1294
|
+
await aiToolAudit
|
|
1295
|
+
.noteApprovalRequested({ conversationId, turnId, callId: event.callId, toolName, location: "server", args: event.args })
|
|
1296
|
+
.catch(() => undefined);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
await pipeline.apply(event);
|
|
1300
|
+
const suspended = await aiConversations.suspendTurn({
|
|
1301
|
+
conversationId,
|
|
1302
|
+
turnId,
|
|
1303
|
+
leaseOwner: this.config.leaseOwner,
|
|
1304
|
+
blocks: pipeline.blocks,
|
|
1305
|
+
seq: pipeline.seq,
|
|
1306
|
+
waitingBudgetMs: AI_ACTION_BUDGET_MS,
|
|
1307
|
+
});
|
|
1308
|
+
await pipeline.flush().catch(() => undefined);
|
|
1309
|
+
return suspended;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private startHeartbeat(conversationId: string, turnId: string, abortController: AbortController): () => void {
|
|
1313
|
+
let stopped = false;
|
|
1314
|
+
let failures = 0;
|
|
1315
|
+
const tick = async () => {
|
|
1316
|
+
if (stopped) return;
|
|
1317
|
+
let ok = false;
|
|
1318
|
+
try {
|
|
1319
|
+
ok = await aiConversations.heartbeatTurn({
|
|
1320
|
+
conversationId,
|
|
1321
|
+
turnId,
|
|
1322
|
+
leaseOwner: this.config.leaseOwner,
|
|
1323
|
+
leaseMs: AI_TURN_LEASE_MS,
|
|
1324
|
+
});
|
|
1325
|
+
failures = 0;
|
|
1326
|
+
} catch {
|
|
1327
|
+
failures += 1;
|
|
1328
|
+
if (failures < 3) return;
|
|
1329
|
+
}
|
|
1330
|
+
if (!ok && !stopped) abortController.abort();
|
|
1331
|
+
};
|
|
1332
|
+
const timer = setInterval(() => void tick(), this.config.heartbeatMs);
|
|
1333
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
1334
|
+
void tick();
|
|
1335
|
+
return () => {
|
|
1336
|
+
stopped = true;
|
|
1337
|
+
clearInterval(timer);
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
private async runCompaction(
|
|
1342
|
+
conversationId: string,
|
|
1343
|
+
turnId: string,
|
|
1344
|
+
pipeline: StreamPipeline,
|
|
1345
|
+
config: Extract<AiTurnRunConfig, { kind: "compact" }>,
|
|
1346
|
+
signal: AbortSignal,
|
|
1347
|
+
): Promise<void> {
|
|
1348
|
+
const abortController = new AbortController();
|
|
1349
|
+
const onSignal = () => abortController.abort();
|
|
1350
|
+
if (signal.aborted) abortController.abort();
|
|
1351
|
+
else signal.addEventListener("abort", onSignal, { once: true });
|
|
1352
|
+
|
|
1353
|
+
let validated: ValidatedTurn;
|
|
1354
|
+
try {
|
|
1355
|
+
validated = await (this.config.validateTurn ?? validateAiTurnRequest)({
|
|
1356
|
+
input: "",
|
|
1357
|
+
modelPolicy: config.modelPolicy,
|
|
1358
|
+
requestedModelId: config.requestedModelId,
|
|
1359
|
+
});
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
signal.removeEventListener("abort", onSignal);
|
|
1362
|
+
await this.finalize(
|
|
1363
|
+
conversationId,
|
|
1364
|
+
turnId,
|
|
1365
|
+
pipeline,
|
|
1366
|
+
"failed",
|
|
1367
|
+
error instanceof Error ? error.message : "AI compaction failed",
|
|
1368
|
+
"compact",
|
|
1369
|
+
);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const { settings, resolved } = validated;
|
|
1373
|
+
|
|
1374
|
+
const store = aiConversations.createSessionStore({
|
|
1375
|
+
conversationId,
|
|
1376
|
+
modelProfileId: resolved.profile.id,
|
|
1377
|
+
turnId,
|
|
1378
|
+
leaseOwner: this.config.leaseOwner,
|
|
1379
|
+
});
|
|
1380
|
+
const loop = compact({
|
|
1381
|
+
agentId: "cloud",
|
|
1382
|
+
loopId: turnId,
|
|
1383
|
+
store,
|
|
1384
|
+
provider: resolved.provider,
|
|
1385
|
+
force: true,
|
|
1386
|
+
signal: abortController.signal,
|
|
1387
|
+
compact: createCloudCompactFn({
|
|
1388
|
+
conversationId,
|
|
1389
|
+
turnId,
|
|
1390
|
+
modelProfileId: resolved.profile.id,
|
|
1391
|
+
additionalInstructions: settings.compactionInstructions,
|
|
1392
|
+
maxOutputTokens: resolved.profile.maxOutputTokens,
|
|
1393
|
+
signal: abortController.signal,
|
|
1394
|
+
// Manual /compact means "make the context small": summarize everything
|
|
1395
|
+
// except the latest loop, so the marker lands right above the newest
|
|
1396
|
+
// messages instead of far up the chat.
|
|
1397
|
+
keepRecentLoops: 1,
|
|
1398
|
+
}),
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
const stopHeartbeat = this.startHeartbeat(conversationId, turnId, abortController);
|
|
1402
|
+
let status: "completed" | "failed" | "aborted" = "failed";
|
|
1403
|
+
let error: string | null = null;
|
|
1404
|
+
try {
|
|
1405
|
+
for await (const event of loop as AsyncIterable<CompactEvent>) {
|
|
1406
|
+
if (event.type === "compaction_start") await pipeline.applyCompaction("running");
|
|
1407
|
+
else if (event.type === "compaction_end") await pipeline.applyCompaction("completed");
|
|
1408
|
+
else if (event.type === "issue") error = event.issue.message;
|
|
1409
|
+
else if (event.type === "loop_end") {
|
|
1410
|
+
status = event.reason === "stop" ? "completed" : event.reason === "aborted" ? "aborted" : "failed";
|
|
1411
|
+
await pipeline.applyCompaction(status === "failed" ? "failed" : "completed", event.result);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
if (abortController.signal.aborted) {
|
|
1416
|
+
status = "aborted";
|
|
1417
|
+
} else {
|
|
1418
|
+
status = "failed";
|
|
1419
|
+
error = err instanceof Error ? err.message : "AI compaction failed";
|
|
1420
|
+
await pipeline.emitError(error).catch(() => undefined);
|
|
1421
|
+
}
|
|
1422
|
+
} finally {
|
|
1423
|
+
stopHeartbeat();
|
|
1424
|
+
signal.removeEventListener("abort", onSignal);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
await this.finalize(conversationId, turnId, pipeline, status, error, "compact");
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// ---------------------------------------------------------------------------
|
|
1432
|
+
// Stream pipeline — seq allocation, ordered publish + snapshot throttle
|
|
1433
|
+
// ---------------------------------------------------------------------------
|
|
1434
|
+
|
|
1435
|
+
class StreamPipeline {
|
|
1436
|
+
blocks: AiTurnBlock[];
|
|
1437
|
+
seq: number;
|
|
1438
|
+
/** Wall-clock ms from construction to the first streamed block op (provider TTFB proxy). */
|
|
1439
|
+
firstBlockMs: number | null = null;
|
|
1440
|
+
private readonly createdAt = Date.now();
|
|
1441
|
+
private readonly conversationId: string;
|
|
1442
|
+
private readonly turnId: string;
|
|
1443
|
+
private readonly attempt: number;
|
|
1444
|
+
private readonly leaseOwner: string;
|
|
1445
|
+
private readonly mapper: ReturnType<typeof createEventMapper>;
|
|
1446
|
+
private readonly allowRememberedApprovals: boolean;
|
|
1447
|
+
private lastSnapshotAt = 0;
|
|
1448
|
+
private snapshotDirty = false;
|
|
1449
|
+
private chain: Promise<void> = Promise.resolve();
|
|
1450
|
+
|
|
1451
|
+
constructor(input: {
|
|
1452
|
+
conversationId: string;
|
|
1453
|
+
turnId: string;
|
|
1454
|
+
attempt: number;
|
|
1455
|
+
startSeq: number;
|
|
1456
|
+
leaseOwner: string;
|
|
1457
|
+
seedBlocks: AiTurnBlock[];
|
|
1458
|
+
allowRememberedApprovals: boolean;
|
|
1459
|
+
}) {
|
|
1460
|
+
this.conversationId = input.conversationId;
|
|
1461
|
+
this.turnId = input.turnId;
|
|
1462
|
+
this.attempt = input.attempt;
|
|
1463
|
+
this.leaseOwner = input.leaseOwner;
|
|
1464
|
+
this.seq = input.startSeq;
|
|
1465
|
+
this.blocks = [];
|
|
1466
|
+
this.allowRememberedApprovals = input.allowRememberedApprovals;
|
|
1467
|
+
this.mapper = createEventMapper(input.attempt, input.seedBlocks, input.allowRememberedApprovals);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
private ordered(run: () => Promise<void>): Promise<void> {
|
|
1471
|
+
this.chain = this.chain.catch(() => undefined).then(run);
|
|
1472
|
+
return this.chain;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
private nextSeq(): number {
|
|
1476
|
+
this.seq += 1;
|
|
1477
|
+
return this.seq;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private envelope<T extends { type: string }>(event: T): T & { v: 1; conversationId: string; turnId: string; attempt: number } {
|
|
1481
|
+
return { ...event, v: 1, conversationId: this.conversationId, turnId: this.turnId, attempt: this.attempt };
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
seedBaseline(blocks: AiTurnBlock[]): void {
|
|
1485
|
+
this.blocks = this.allowRememberedApprovals
|
|
1486
|
+
? blocks
|
|
1487
|
+
: blocks.map((block) =>
|
|
1488
|
+
block.kind === "tool" && block.approval ? { ...block, approval: { ...block.approval, allowAlways: false } } : block,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
setFrontendModes(modes: Map<string, AiFrontendToolMode>): void {
|
|
1493
|
+
this.mapper.setFrontendModes(modes);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
setPresentations(presentations: Map<string, AiToolPresentation>): void {
|
|
1497
|
+
this.mapper.setPresentations(presentations);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
setCanonicalNames(names: Map<string, string>): void {
|
|
1501
|
+
this.mapper.setCanonicalNames(names);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
setApprovalReviews(reviews: Map<string, CapabilityActionReview>): void {
|
|
1505
|
+
this.mapper.setApprovalReviews(reviews);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
setApprovalPolicies(policies: PreparedAiTools["approvalPolicies"]): void {
|
|
1509
|
+
this.mapper.setApprovalPolicies(policies);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
setRejectedCallIds(callIds: Set<string>): void {
|
|
1513
|
+
this.mapper.setRejectedCallIds(callIds);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
async emitBaseline(): Promise<void> {
|
|
1517
|
+
for (const block of this.blocks) {
|
|
1518
|
+
const seq = this.nextSeq();
|
|
1519
|
+
await this.publish(this.envelope({ type: "block_set" as const, seq, block }) as AiWireEvent);
|
|
1520
|
+
}
|
|
1521
|
+
this.snapshotDirty = this.blocks.length > 0;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async emitTurnStarted(modelProfileId: string): Promise<void> {
|
|
1525
|
+
const seq = this.nextSeq();
|
|
1526
|
+
await this.publish(
|
|
1527
|
+
this.envelope({ type: "turn_started" as const, seq, modelProfileId, providerModel: "", blocks: this.blocks }) as AiWireEvent,
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
async apply(event: OutboundEvent): Promise<void> {
|
|
1532
|
+
const ops = this.mapper.translate(event);
|
|
1533
|
+
if (ops.length > 0 && this.firstBlockMs === null) this.firstBlockMs = Date.now() - this.createdAt;
|
|
1534
|
+
for (const op of ops) await this.emitOp(op);
|
|
1535
|
+
await this.maybeSnapshot();
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
async applySteer(steer: AiTurnSteer): Promise<void> {
|
|
1539
|
+
await this.emitOp({
|
|
1540
|
+
type: "block_set",
|
|
1541
|
+
block: {
|
|
1542
|
+
id: steerMessageBlockId(steer.id),
|
|
1543
|
+
kind: "steer_message",
|
|
1544
|
+
steerId: steer.id,
|
|
1545
|
+
text: steer.text,
|
|
1546
|
+
status: "consumed",
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
await this.emitOp({
|
|
1550
|
+
type: "block_set",
|
|
1551
|
+
block: { id: steerAppliedBlockId(steer.id), kind: "steer_applied", steerId: steer.id },
|
|
1552
|
+
});
|
|
1553
|
+
await this.maybeSnapshot();
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
async applyCompaction(
|
|
1557
|
+
status: "running" | "completed" | "failed",
|
|
1558
|
+
result?: Extract<AiTurnBlock, { kind: "compaction" }>["result"],
|
|
1559
|
+
): Promise<void> {
|
|
1560
|
+
await this.emitOp(this.mapper.compaction(status, result));
|
|
1561
|
+
await this.maybeSnapshot();
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
private async emitOp(
|
|
1565
|
+
op: { type: "block_set"; block: AiTurnBlock } | { type: "block_delta"; blockId: string; blockKind: "text" | "thinking"; delta: string },
|
|
1566
|
+
): Promise<void> {
|
|
1567
|
+
const seq = this.nextSeq();
|
|
1568
|
+
const event = this.envelope({ ...op, seq }) as AiWireEvent;
|
|
1569
|
+
this.blocks = applyWireEventToBlocks(this.blocks, event);
|
|
1570
|
+
this.snapshotDirty = true;
|
|
1571
|
+
await this.publish(event);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private async maybeSnapshot(): Promise<void> {
|
|
1575
|
+
if (!this.snapshotDirty) return;
|
|
1576
|
+
if (Date.now() - this.lastSnapshotAt < AI_SNAPSHOT_INTERVAL_MS) return;
|
|
1577
|
+
await this.persistSnapshot();
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async persistSnapshot(): Promise<void> {
|
|
1581
|
+
this.lastSnapshotAt = Date.now();
|
|
1582
|
+
this.snapshotDirty = false;
|
|
1583
|
+
await aiConversations
|
|
1584
|
+
.saveTurnLiveState({
|
|
1585
|
+
conversationId: this.conversationId,
|
|
1586
|
+
turnId: this.turnId,
|
|
1587
|
+
leaseOwner: this.leaseOwner,
|
|
1588
|
+
blocks: this.blocks,
|
|
1589
|
+
seq: this.seq,
|
|
1590
|
+
})
|
|
1591
|
+
.catch(() => undefined);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
async emitError(message: string): Promise<void> {
|
|
1595
|
+
const seq = this.nextSeq();
|
|
1596
|
+
const block: AiTurnBlock = { id: `error-${this.attempt}-${seq}`, kind: "text", text: `⚠️ ${message}` };
|
|
1597
|
+
const event = this.envelope({ type: "block_set" as const, seq, block }) as AiWireEvent;
|
|
1598
|
+
this.blocks = applyWireEventToBlocks(this.blocks, event);
|
|
1599
|
+
await this.publish(event);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async emitTurnFinished(status: "completed" | "failed" | "aborted", error: string | null): Promise<void> {
|
|
1603
|
+
const seq = this.nextSeq();
|
|
1604
|
+
await this.publish(this.envelope({ type: "turn_finished" as const, seq, status, error }) as AiWireEvent);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private publish(event: AiWireEvent): Promise<void> {
|
|
1608
|
+
return this.ordered(() => publishAiWireEvent(event).catch(() => undefined));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
async flush(): Promise<void> {
|
|
1612
|
+
await this.chain;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
export const __aiExecutorTest = {
|
|
1617
|
+
indexConversationToolSource,
|
|
1618
|
+
StreamPipeline,
|
|
1619
|
+
applyToolRoundPolicy,
|
|
1620
|
+
createEventMapper,
|
|
1621
|
+
rebuildAttemptBaseline,
|
|
1622
|
+
rebuildBlocksFromMessages,
|
|
1623
|
+
toolRoundState,
|
|
1624
|
+
};
|