@raybase/core 0.0.1 → 0.3.3
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/LICENSE +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
package/dist/agent.js
ADDED
|
@@ -0,0 +1,3123 @@
|
|
|
1
|
+
import { jsonSchema, tool } from "ai";
|
|
2
|
+
import { agentPrincipal } from "./principal.js";
|
|
3
|
+
import { actionInputJsonSchema, agentActionRegistrations, executeAction, restoreStrictActionInput, unsupportedRaybaseAgentInvocationRuntime } from "./actions.js";
|
|
4
|
+
import { authSecret, createRaybaseAuthContextRuntime, unsupportedRaybaseAuthContextRuntime } from "./auth.js";
|
|
5
|
+
import { agentActionToolName, agentSkillActionBindings, agentSkillRegistrations, agentSkillSummaries, validateAgentSkillsForAgent } from "./agent-skills.js";
|
|
6
|
+
import { agentRemoteMcpBindingSummaries, agentRemoteMcpToolBindings, validateAgentRemoteMcpBindings } from "./agent-remote-mcp.js";
|
|
7
|
+
import { agentMemoryMaintenanceRuns, agentMemorySpaces, raybaseAgentMemorySystemTables, raybaseAgentMemoryToolNames, validateAgentMemoryForAgent } from "./agent-memory.js";
|
|
8
|
+
import { raybaseAgentSkillSystemTables, raybaseAgentSkillToolNames, validateAgentSkillStoreForAgent } from "./agent-skill-store.js";
|
|
9
|
+
import { raybaseAgentWakeSystemTables, raybaseAgentWakeToolNames, validateAgentWakesForAgent } from "./agent-wakes.js";
|
|
10
|
+
import { raybaseAgentFollowUpSystemTables, validateAgentFollowUpsForAgent } from "./agent-follow-ups.js";
|
|
11
|
+
import { findAgentRoomMember, raybaseAgentRoomSystemTables, setAgentRoomMemberPinned, validateAgentRoomsForAgent } from "./agent-rooms.js";
|
|
12
|
+
import { buildAgentChapterRehydrateSeed, defaultAgentRunEvidenceTtlMs, loadLastAgentChapter, minAgentRunEvidenceTtlMs, normalizeAgentRetentionConfig, purgeAgentChaptersForConversation, raybaseAgentChapterSystemTables, validateAgentRetentionForAgent } from "./agent-chapters.js";
|
|
13
|
+
import { ensureAgentChapterSearch } from "./agent-chapter-search.js";
|
|
14
|
+
import { raybaseAgentChapterArchiveSystemTables } from "./agent-chapter-archive.js";
|
|
15
|
+
import { normalizeAgentVoiceNotesConfig } from "./agent-voice-notes.js";
|
|
16
|
+
import { validateAgentUserDataPartsConfig } from "./agent-user-data-parts.js";
|
|
17
|
+
export { isAgentUserDataPartType, isReservedAgentDataPartType, maxAgentUserDataBytesPerMessage, maxAgentUserDataPartBytes, maxAgentUserDataPartsPerMessage } from "./agent-user-data-parts.js";
|
|
18
|
+
import { parseSearchQuery } from "./search.js";
|
|
19
|
+
import { storageHardMaxSizeBytes } from "./storage.js";
|
|
20
|
+
import { estimateTokensFromChars, normalizeAiDefaults, normalizeAiLimits, recordAiActionEvent } from "./ai.js";
|
|
21
|
+
import { sha256Hex, stableJson, validateRaybaseName } from "./internal.js";
|
|
22
|
+
import { validateJsonSchema } from "./json-schema.js";
|
|
23
|
+
import { nextCronRun, validateCron } from "./jobs.js";
|
|
24
|
+
import { id, integer, longText, raybaseSystemTable, text } from "./schema.js";
|
|
25
|
+
import { setting } from "./settings.js";
|
|
26
|
+
import { nowIso, q, randomId } from "./sql.js";
|
|
27
|
+
import { appNowMs, wallNowIso, wallNowMs } from "./clock.js";
|
|
28
|
+
export const defaultAgentCompaction = {
|
|
29
|
+
enabled: true,
|
|
30
|
+
keepRecentMessages: 16,
|
|
31
|
+
triggerRatio: 0.7,
|
|
32
|
+
minSummaryOutputTokens: 800,
|
|
33
|
+
summarySpanRatio: 0.12,
|
|
34
|
+
maxSummaryOutputTokens: 4_000
|
|
35
|
+
};
|
|
36
|
+
/** Default cap for custom prompt bands. The built-in head has no default cap. */
|
|
37
|
+
export const defaultAgentPromptBandMaxChars = 16_000;
|
|
38
|
+
export const defaultAgentDeliveryMaxReplacementChars = 32_000;
|
|
39
|
+
export const maxAgentDeliveryReplacementChars = 128_000;
|
|
40
|
+
export const defaultAgentAttachmentMediaTypes = [
|
|
41
|
+
"image/jpeg",
|
|
42
|
+
"image/png",
|
|
43
|
+
"image/webp",
|
|
44
|
+
"image/gif"
|
|
45
|
+
];
|
|
46
|
+
export const defaultAgentAttachmentMaxPerMessage = 4;
|
|
47
|
+
export const defaultAgentAttachmentMaxBytesPerFile = 8 * 1024 * 1024;
|
|
48
|
+
export const defaultAgentAttachmentRecentMessages = 8;
|
|
49
|
+
export const raybaseAiChatHarness = {
|
|
50
|
+
runtime: "ai-chat",
|
|
51
|
+
durableObjectBinding: "RAYBASE_AI_CHAT_AGENT",
|
|
52
|
+
triggerType: "chat",
|
|
53
|
+
instanceStrategy: "conversation"
|
|
54
|
+
};
|
|
55
|
+
export const raybaseAgentInternalReadPath = "/__raybase_agent_read";
|
|
56
|
+
export const raybaseAgentInternalUserReadPath = "/__raybase_agent_user_read";
|
|
57
|
+
export const raybaseAgentInternalCleanupPath = "/__raybase_agent_cleanup";
|
|
58
|
+
export const raybaseAgentInternalUserPurgePath = "/__raybase_agent_user_purge";
|
|
59
|
+
export const raybaseAgentInternalRehydratePath = "/__raybase_agent_rehydrate";
|
|
60
|
+
export const raybaseAgentInternalRoomNoticePath = "/__raybase_agent_room_notice";
|
|
61
|
+
export const raybaseAgentInternalRoomActivityPath = "/__raybase_agent_room_activity";
|
|
62
|
+
export const raybaseAgentInternalRoomMembershipPath = "/__raybase_agent_room_membership";
|
|
63
|
+
export const raybaseAgentInternalRoomRedactPath = "/__raybase_agent_room_redact";
|
|
64
|
+
export const raybaseAgentSystemTables = [
|
|
65
|
+
...raybaseAgentChapterSystemTables,
|
|
66
|
+
...raybaseAgentChapterArchiveSystemTables,
|
|
67
|
+
...raybaseAgentRoomSystemTables,
|
|
68
|
+
raybaseSystemTable("_agent_prompt_bands", {
|
|
69
|
+
id: id(),
|
|
70
|
+
agentName: text({ required: true }),
|
|
71
|
+
conversationId: text({ required: true }),
|
|
72
|
+
userId: text(),
|
|
73
|
+
bandName: text({ required: true }),
|
|
74
|
+
epochKey: text({ required: true }),
|
|
75
|
+
included: integer({ required: true }),
|
|
76
|
+
content: longText(),
|
|
77
|
+
totalChars: integer(),
|
|
78
|
+
truncatedAt: integer(),
|
|
79
|
+
updatedAt: text({ required: true })
|
|
80
|
+
}, {
|
|
81
|
+
indexes: [
|
|
82
|
+
["agentName", "conversationId"],
|
|
83
|
+
["userId"],
|
|
84
|
+
["conversationId", "bandName"]
|
|
85
|
+
]
|
|
86
|
+
}),
|
|
87
|
+
raybaseSystemTable("_agent_instances", {
|
|
88
|
+
id: id(),
|
|
89
|
+
conversationId: text({ required: true, unique: true }),
|
|
90
|
+
agentName: text({ required: true }),
|
|
91
|
+
agentTitle: text({ required: true }),
|
|
92
|
+
agentOwner: text(),
|
|
93
|
+
moduleId: text(),
|
|
94
|
+
runtime: text({ required: true }),
|
|
95
|
+
triggerType: text({ required: true }),
|
|
96
|
+
instanceKey: text({ required: true, unique: true }),
|
|
97
|
+
principalKind: text({ required: true }),
|
|
98
|
+
userId: text(),
|
|
99
|
+
publicScopeHash: text(),
|
|
100
|
+
organizationId: text(),
|
|
101
|
+
route: text(),
|
|
102
|
+
surface: text({ required: true }),
|
|
103
|
+
status: text({ required: true }),
|
|
104
|
+
cleanupRequestedAt: text(),
|
|
105
|
+
hotStorageDeletedAt: text(),
|
|
106
|
+
deletedAt: text(),
|
|
107
|
+
retentionReason: text(),
|
|
108
|
+
pinnedAt: text(),
|
|
109
|
+
createdAt: text({ required: true }),
|
|
110
|
+
updatedAt: text({ required: true }),
|
|
111
|
+
lastActiveAt: text({ required: true }),
|
|
112
|
+
expiresAt: text(),
|
|
113
|
+
lastRunId: text(),
|
|
114
|
+
lastError: longText(),
|
|
115
|
+
metadataJson: longText(),
|
|
116
|
+
entryKind: text(),
|
|
117
|
+
entryJson: longText(),
|
|
118
|
+
kind: text(),
|
|
119
|
+
roomMembershipRev: integer(),
|
|
120
|
+
lastReadAt: text()
|
|
121
|
+
}, {
|
|
122
|
+
indexes: [
|
|
123
|
+
["agentName", "conversationId"],
|
|
124
|
+
["principalKind", "userId", "updatedAt"],
|
|
125
|
+
["principalKind", "publicScopeHash", "updatedAt"],
|
|
126
|
+
["status", "expiresAt"],
|
|
127
|
+
["instanceKey"]
|
|
128
|
+
]
|
|
129
|
+
}),
|
|
130
|
+
raybaseSystemTable("_agent_runs", {
|
|
131
|
+
runId: id(),
|
|
132
|
+
conversationId: text({ required: true }),
|
|
133
|
+
agentName: text({ required: true }),
|
|
134
|
+
requestId: text({ required: true }),
|
|
135
|
+
triggerType: text({ required: true }),
|
|
136
|
+
inputMessageId: text(),
|
|
137
|
+
status: text({ required: true }),
|
|
138
|
+
startedAt: text({ required: true }),
|
|
139
|
+
/**
|
|
140
|
+
* The last moment this run was known to be making progress.
|
|
141
|
+
*
|
|
142
|
+
* `startedAt` cannot answer "is it still alive": it never moves, so a
|
|
143
|
+
* turn working steadily for six minutes and a turn whose writer died in
|
|
144
|
+
* its first second look identical to any reader outside the isolate. The
|
|
145
|
+
* staleness sweep read `startedAt`, could not tell those apart, and said
|
|
146
|
+
* so in its own comments — then failed live runs anyway (found 2026-08-25:
|
|
147
|
+
* a member watched "Birch ran into a problem while answering" arrive 24
|
|
148
|
+
* seconds before the same run delivered its answer).
|
|
149
|
+
*
|
|
150
|
+
* It is touched per model step rather than on a timer, and that is the
|
|
151
|
+
* point: a wedged turn stops beating and is still reaped, which is what
|
|
152
|
+
* the horizon is for. Null on rows written before the column existed, and
|
|
153
|
+
* every predicate coalesces to `startedAt` so those behave exactly as
|
|
154
|
+
* they used to.
|
|
155
|
+
*/
|
|
156
|
+
heartbeatAt: text(),
|
|
157
|
+
finishedAt: text(),
|
|
158
|
+
error: longText(),
|
|
159
|
+
usageEventId: text(),
|
|
160
|
+
auditEventId: text(),
|
|
161
|
+
metadataJson: longText()
|
|
162
|
+
}, { indexes: [["conversationId", "startedAt"], ["agentName", "startedAt"], ["status", "startedAt"], ["requestId"]] }),
|
|
163
|
+
raybaseSystemTable("_agent_requests", {
|
|
164
|
+
id: id(),
|
|
165
|
+
conversationId: text({ required: true }),
|
|
166
|
+
requestKey: text({ required: true }),
|
|
167
|
+
messageId: text({ required: true }),
|
|
168
|
+
kind: text(),
|
|
169
|
+
authorUserId: text(),
|
|
170
|
+
disposition: text(),
|
|
171
|
+
runId: text(),
|
|
172
|
+
seq: integer(),
|
|
173
|
+
createdAt: text({ required: true }),
|
|
174
|
+
updatedAt: text({ required: true })
|
|
175
|
+
}, {
|
|
176
|
+
indexes: [
|
|
177
|
+
{ columns: ["conversationId", "requestKey"], unique: true },
|
|
178
|
+
["conversationId", "createdAt"],
|
|
179
|
+
["runId"]
|
|
180
|
+
]
|
|
181
|
+
}),
|
|
182
|
+
raybaseSystemTable("_agent_limit_windows", {
|
|
183
|
+
id: id(),
|
|
184
|
+
scope: text({ required: true }),
|
|
185
|
+
scopeId: text({ required: true }),
|
|
186
|
+
windowStart: text({ required: true }),
|
|
187
|
+
windowEnd: text({ required: true }),
|
|
188
|
+
requestCount: integer({ required: true }),
|
|
189
|
+
estimatedOutputTokens: integer({ required: true }),
|
|
190
|
+
updatedAt: text({ required: true })
|
|
191
|
+
}, { indexes: [{ columns: ["scope", "scopeId", "windowStart"], unique: true }, ["windowEnd"]] }),
|
|
192
|
+
raybaseSystemTable("_agent_schedules", {
|
|
193
|
+
id: id(),
|
|
194
|
+
agentName: text({ required: true }),
|
|
195
|
+
triggerIndex: integer({ required: true }),
|
|
196
|
+
triggerKey: text({ required: true }),
|
|
197
|
+
inputJson: longText(),
|
|
198
|
+
cron: text({ required: true }),
|
|
199
|
+
timezone: text(),
|
|
200
|
+
enabled: integer({ required: true }),
|
|
201
|
+
userMessage: longText(),
|
|
202
|
+
cronOverride: text(),
|
|
203
|
+
timezoneOverride: text(),
|
|
204
|
+
enabledOverride: integer(),
|
|
205
|
+
inputJsonOverride: longText(),
|
|
206
|
+
inputJsonOverrideEnabled: integer(),
|
|
207
|
+
userMessageOverride: longText(),
|
|
208
|
+
lastRunAt: text(),
|
|
209
|
+
nextRunAt: text(),
|
|
210
|
+
service: text(),
|
|
211
|
+
dispatchGeneration: integer(),
|
|
212
|
+
dispatchAttempts: integer(),
|
|
213
|
+
dispatchLeaseExpiresAt: text(),
|
|
214
|
+
lastDispatchAt: text(),
|
|
215
|
+
lastDispatchError: longText(),
|
|
216
|
+
createdAt: text({ required: true }),
|
|
217
|
+
updatedAt: text({ required: true })
|
|
218
|
+
}, {
|
|
219
|
+
indexes: [
|
|
220
|
+
["agentName", "triggerIndex"],
|
|
221
|
+
["enabled", "nextRunAt"],
|
|
222
|
+
["enabledOverride", "nextRunAt"],
|
|
223
|
+
["triggerKey"]
|
|
224
|
+
]
|
|
225
|
+
}),
|
|
226
|
+
raybaseSystemTable("_agent_trigger_events", {
|
|
227
|
+
id: id(),
|
|
228
|
+
agentName: text({ required: true }),
|
|
229
|
+
triggerType: text({ required: true }),
|
|
230
|
+
triggerKey: text({ required: true }),
|
|
231
|
+
sourceTable: text(),
|
|
232
|
+
sourceOp: text(),
|
|
233
|
+
rowId: text(),
|
|
234
|
+
inputJson: longText(),
|
|
235
|
+
idempotencyKey: text({ unique: true }),
|
|
236
|
+
status: text({ required: true }),
|
|
237
|
+
runId: text(),
|
|
238
|
+
error: longText(),
|
|
239
|
+
createdAt: text({ required: true }),
|
|
240
|
+
updatedAt: text({ required: true })
|
|
241
|
+
}, {
|
|
242
|
+
indexes: [
|
|
243
|
+
["agentName", "createdAt"],
|
|
244
|
+
["triggerType", "status"],
|
|
245
|
+
["sourceTable", "sourceOp", "createdAt"],
|
|
246
|
+
["runId"]
|
|
247
|
+
]
|
|
248
|
+
}),
|
|
249
|
+
...raybaseAgentWakeSystemTables,
|
|
250
|
+
...raybaseAgentFollowUpSystemTables,
|
|
251
|
+
...raybaseAgentMemorySystemTables,
|
|
252
|
+
...raybaseAgentSkillSystemTables
|
|
253
|
+
];
|
|
254
|
+
export const defaultAgentRunHistoryLimit = 20;
|
|
255
|
+
export const maxAgentRunHistoryLimit = 100;
|
|
256
|
+
const agentNamePattern = /^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/;
|
|
257
|
+
const encoder = new TextEncoder();
|
|
258
|
+
const decoder = new TextDecoder();
|
|
259
|
+
const defaultConversationTtlSeconds = 60 * 60 * 4;
|
|
260
|
+
const websocketBootstrapTtlSeconds = 30;
|
|
261
|
+
const staleDeletingRetrySeconds = 10 * 60;
|
|
262
|
+
const retryableCleanupStatuses = new Set(["active", "cleanupRequested", "cleanupError"]);
|
|
263
|
+
const dataTriggerEventPattern = /^([A-Za-z][A-Za-z0-9_:-]*)\.(create|update|delete)$/;
|
|
264
|
+
const agentEntryKindPattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
265
|
+
export const maxAgentEntryPayloadBytes = 4 * 1024;
|
|
266
|
+
export const maxAgentEntryKickoffChars = 4 * 1024;
|
|
267
|
+
// Composed-turn and entry-kickoff daily caps ship with no default: unset means
|
|
268
|
+
// unlimited. Both are runaway-loop guards in quota clothing, and the runtime
|
|
269
|
+
// already serializes one turn per conversation and bounds loop steps per turn.
|
|
270
|
+
// Apps that want a cap declare limits.composedPerDay / limits.entryKickoffPerDay
|
|
271
|
+
// and get exactly the enforcement below.
|
|
272
|
+
export function defineAgent(definition) {
|
|
273
|
+
validateAgentName(definition.name);
|
|
274
|
+
const runtime = definition.runtime;
|
|
275
|
+
if (runtime !== "ai-chat")
|
|
276
|
+
throw new Error(`Unsupported Raybase agent runtime: ${String(runtime)}`);
|
|
277
|
+
const unexpected = definition;
|
|
278
|
+
if (unexpected.scope !== undefined || unexpected.instance !== undefined) {
|
|
279
|
+
throw new Error("Raybase agent scope and instance fields are internal and cannot be declared by apps");
|
|
280
|
+
}
|
|
281
|
+
if (definition.prompt && definition.system !== undefined) {
|
|
282
|
+
throw new Error("Raybase agents cannot declare both system and prompt; use prompt.default for configurable prompt defaults");
|
|
283
|
+
}
|
|
284
|
+
if (definition.prompt?.configurable === true && !definition.prompt.default.trim()) {
|
|
285
|
+
throw new Error("Configurable Raybase agent prompts must provide a non-empty default prompt");
|
|
286
|
+
}
|
|
287
|
+
validateAgentConversationTitles(definition);
|
|
288
|
+
validateAgentSkillsForAgent(definition);
|
|
289
|
+
validateAgentSkillStoreForAgent(definition);
|
|
290
|
+
validateAgentRemoteMcpBindings(definition);
|
|
291
|
+
validateAgentMemoryForAgent(definition);
|
|
292
|
+
validateAgentWakesForAgent(definition);
|
|
293
|
+
validateAgentFollowUpsForAgent(definition);
|
|
294
|
+
validateAgentRetentionForAgent(definition);
|
|
295
|
+
validateAgentAllRuntimeToolNames(definition);
|
|
296
|
+
validateAgentCompaction(definition.compaction);
|
|
297
|
+
if (definition.limits?.composedPerDay !== undefined
|
|
298
|
+
&& (!Number.isInteger(definition.limits.composedPerDay) || definition.limits.composedPerDay < 1)) {
|
|
299
|
+
throw new Error(`Raybase agent ${definition.name} limits.composedPerDay must be a positive integer`);
|
|
300
|
+
}
|
|
301
|
+
if (definition.limits?.entryKickoffPerDay !== undefined
|
|
302
|
+
&& (!Number.isInteger(definition.limits.entryKickoffPerDay) || definition.limits.entryKickoffPerDay < 1)) {
|
|
303
|
+
throw new Error(`Raybase agent ${definition.name} limits.entryKickoffPerDay must be a positive integer`);
|
|
304
|
+
}
|
|
305
|
+
if (definition.limits?.loopReserveChars !== undefined
|
|
306
|
+
&& (!Number.isInteger(definition.limits.loopReserveChars) || definition.limits.loopReserveChars < 0)) {
|
|
307
|
+
throw new Error(`Raybase agent ${definition.name} limits.loopReserveChars must be a non-negative integer`);
|
|
308
|
+
}
|
|
309
|
+
if (definition.compose?.renderContext !== undefined && typeof definition.compose.renderContext !== "boolean") {
|
|
310
|
+
throw new Error(`Raybase agent ${definition.name} compose.renderContext must be a boolean`);
|
|
311
|
+
}
|
|
312
|
+
if (definition.timeSentinel !== undefined && typeof definition.timeSentinel !== "boolean") {
|
|
313
|
+
throw new Error(`Raybase agent ${definition.name} timeSentinel must be a boolean`);
|
|
314
|
+
}
|
|
315
|
+
validateAgentExtensionPoints(definition);
|
|
316
|
+
const scopes = normalizeAgentScopes(definition.scopes, definition.name);
|
|
317
|
+
const triggers = normalizeAgentTriggers(definition.triggers, agentMemoryMaintenanceRuns(definition));
|
|
318
|
+
const normalized = { ...definition, runtime, triggers, ...(scopes ? { scopes } : {}) };
|
|
319
|
+
validateAgentEntriesForAgent(normalized);
|
|
320
|
+
validateAgentRoomsForAgent(normalized);
|
|
321
|
+
return normalized;
|
|
322
|
+
}
|
|
323
|
+
function normalizeAgentScopes(scopes, agentName) {
|
|
324
|
+
if (scopes === undefined)
|
|
325
|
+
return undefined;
|
|
326
|
+
if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || !scope.trim())) {
|
|
327
|
+
throw new Error(`Raybase agent ${agentName} scopes must be an array of non-empty strings`);
|
|
328
|
+
}
|
|
329
|
+
return [...new Set(scopes.map((scope) => scope.trim()))];
|
|
330
|
+
}
|
|
331
|
+
function validateAgentExtensionPoints(agent) {
|
|
332
|
+
if (agent.turnState) {
|
|
333
|
+
if (typeof agent.turnState.load !== "function")
|
|
334
|
+
throw new Error(`Raybase agent ${agent.name} turnState.load must be a function`);
|
|
335
|
+
validateAgentHookTimeout(agent.name, "turnState", agent.turnState.timeoutMs);
|
|
336
|
+
validateAgentHookFailure(agent.name, "turnState", agent.turnState.onFailure);
|
|
337
|
+
const seenServicePrincipals = new Set();
|
|
338
|
+
for (const service of agent.turnState.servicePrincipals ?? []) {
|
|
339
|
+
if (!/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/.test(service)) {
|
|
340
|
+
throw new Error(`Raybase agent ${agent.name} turnState.servicePrincipals contains invalid service name ${String(service)}`);
|
|
341
|
+
}
|
|
342
|
+
if (seenServicePrincipals.has(service)) {
|
|
343
|
+
throw new Error(`Raybase agent ${agent.name} turnState.servicePrincipals declares ${service} more than once`);
|
|
344
|
+
}
|
|
345
|
+
seenServicePrincipals.add(service);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (agent.promptHead !== undefined
|
|
349
|
+
&& (!agent.promptHead
|
|
350
|
+
|| typeof agent.promptHead !== "object"
|
|
351
|
+
|| Array.isArray(agent.promptHead)
|
|
352
|
+
|| (agent.promptHead.epoch !== "chapter" && agent.promptHead.epoch !== "turn"))) {
|
|
353
|
+
throw new Error(`Raybase agent ${agent.name} promptHead.epoch must be chapter or turn`);
|
|
354
|
+
}
|
|
355
|
+
if (agent.promptHead?.maxChars !== undefined
|
|
356
|
+
&& (!Number.isInteger(agent.promptHead.maxChars) || agent.promptHead.maxChars < 1)) {
|
|
357
|
+
throw new Error(`Raybase agent ${agent.name} promptHead.maxChars must be a positive integer`);
|
|
358
|
+
}
|
|
359
|
+
if (agent.promptHead?.onOverflow !== undefined
|
|
360
|
+
&& agent.promptHead.onOverflow !== "truncate"
|
|
361
|
+
&& agent.promptHead.onOverflow !== "error") {
|
|
362
|
+
throw new Error(`Raybase agent ${agent.name} promptHead.onOverflow must be truncate or error`);
|
|
363
|
+
}
|
|
364
|
+
validateNamedAgentHooks(agent.name, "prompt band", agent.promptBands, ["chapter", "turn"]);
|
|
365
|
+
validateNamedAgentHooks(agent.name, "sentinel", agent.sentinels);
|
|
366
|
+
if (agent.delivery) {
|
|
367
|
+
if (agent.delivery.mode !== undefined && typeof agent.delivery.mode !== "function") {
|
|
368
|
+
throw new Error(`Raybase agent ${agent.name} delivery.mode must be a function`);
|
|
369
|
+
}
|
|
370
|
+
if (agent.delivery.gate !== undefined && typeof agent.delivery.gate !== "function") {
|
|
371
|
+
throw new Error(`Raybase agent ${agent.name} delivery.gate must be a function`);
|
|
372
|
+
}
|
|
373
|
+
if (agent.delivery.maxRetries !== undefined
|
|
374
|
+
&& (!Number.isInteger(agent.delivery.maxRetries) || agent.delivery.maxRetries < 0 || agent.delivery.maxRetries > 2)) {
|
|
375
|
+
throw new Error(`Raybase agent ${agent.name} delivery.maxRetries must be an integer between 0 and 2`);
|
|
376
|
+
}
|
|
377
|
+
if (agent.delivery.maxReplacementChars !== undefined
|
|
378
|
+
&& (!Number.isInteger(agent.delivery.maxReplacementChars)
|
|
379
|
+
|| agent.delivery.maxReplacementChars < 1
|
|
380
|
+
|| agent.delivery.maxReplacementChars > maxAgentDeliveryReplacementChars)) {
|
|
381
|
+
throw new Error(`Raybase agent ${agent.name} delivery.maxReplacementChars must be a positive integer no greater than ${maxAgentDeliveryReplacementChars}`);
|
|
382
|
+
}
|
|
383
|
+
if (agent.delivery.fallback !== undefined && agent.delivery.fallback !== "deliver" && agent.delivery.fallback !== "drop") {
|
|
384
|
+
throw new Error(`Raybase agent ${agent.name} delivery.fallback must be deliver or drop`);
|
|
385
|
+
}
|
|
386
|
+
if (agent.delivery.silenceSentinel !== undefined) {
|
|
387
|
+
const sentinel = agent.delivery.silenceSentinel;
|
|
388
|
+
for (const field of ["text", "silence"]) {
|
|
389
|
+
const value = sentinel?.[field];
|
|
390
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 64 || value.trim() !== value) {
|
|
391
|
+
throw new Error(`Raybase agent ${agent.name} delivery.silenceSentinel.${field} must be a trimmed string of 1 to 64 characters`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
validateAgentHookTimeout(agent.name, "delivery", agent.delivery.timeoutMs);
|
|
396
|
+
validateAgentHookFailure(agent.name, "delivery", agent.delivery.onFailure);
|
|
397
|
+
}
|
|
398
|
+
validateAgentConversationsConfig(agent);
|
|
399
|
+
if (agent.chapterClose) {
|
|
400
|
+
for (const key of ["prompt"]) {
|
|
401
|
+
const value = agent.chapterClose[key];
|
|
402
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim())) {
|
|
403
|
+
throw new Error(`Raybase agent ${agent.name} chapterClose.${key} must be a non-empty string`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function validateAgentConversationsConfig(agent) {
|
|
409
|
+
const conversations = agent.conversations;
|
|
410
|
+
if (conversations === undefined)
|
|
411
|
+
return;
|
|
412
|
+
if (!conversations || typeof conversations !== "object" || Array.isArray(conversations)) {
|
|
413
|
+
throw new Error(`Raybase agent ${agent.name} conversations must be an object`);
|
|
414
|
+
}
|
|
415
|
+
if (conversations.eligibility !== undefined && typeof conversations.eligibility !== "function") {
|
|
416
|
+
throw new Error(`Raybase agent ${agent.name} conversations.eligibility must be a function`);
|
|
417
|
+
}
|
|
418
|
+
if (conversations.onCommit !== undefined && typeof conversations.onCommit !== "function") {
|
|
419
|
+
throw new Error(`Raybase agent ${agent.name} conversations.onCommit must be a function`);
|
|
420
|
+
}
|
|
421
|
+
if (conversations.onSilence !== undefined && typeof conversations.onSilence !== "function") {
|
|
422
|
+
throw new Error(`Raybase agent ${agent.name} conversations.onSilence must be a function`);
|
|
423
|
+
}
|
|
424
|
+
if (conversations.onRead !== undefined && typeof conversations.onRead !== "function") {
|
|
425
|
+
throw new Error(`Raybase agent ${agent.name} conversations.onRead must be a function`);
|
|
426
|
+
}
|
|
427
|
+
if (conversations.onMessageMutation !== undefined && typeof conversations.onMessageMutation !== "function") {
|
|
428
|
+
throw new Error(`Raybase agent ${agent.name} conversations.onMessageMutation must be a function`);
|
|
429
|
+
}
|
|
430
|
+
validateAgentHookTimeout(agent.name, "conversations.eligibility", conversations.eligibilityTimeoutMs);
|
|
431
|
+
validateAgentHookFailure(agent.name, "conversations.eligibility", conversations.onEligibilityFailure);
|
|
432
|
+
const messages = conversations.messages;
|
|
433
|
+
if (messages === undefined)
|
|
434
|
+
return;
|
|
435
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) {
|
|
436
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages must be an object`);
|
|
437
|
+
}
|
|
438
|
+
for (const key of ["edit", "remove"]) {
|
|
439
|
+
const config = messages[key];
|
|
440
|
+
if (config === undefined)
|
|
441
|
+
continue;
|
|
442
|
+
if (!config
|
|
443
|
+
|| typeof config !== "object"
|
|
444
|
+
|| Array.isArray(config)
|
|
445
|
+
|| !Number.isInteger(config.windowMs)
|
|
446
|
+
|| config.windowMs < 1) {
|
|
447
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.${key}.windowMs must be a positive integer`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (messages.reactions !== undefined
|
|
451
|
+
&& (!messages.reactions
|
|
452
|
+
|| typeof messages.reactions !== "object"
|
|
453
|
+
|| Array.isArray(messages.reactions)
|
|
454
|
+
|| messages.reactions.enabled !== true
|
|
455
|
+
|| (messages.reactions.agentTool !== undefined && typeof messages.reactions.agentTool !== "boolean"))) {
|
|
456
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.reactions must enable reactions and may set agentTool`);
|
|
457
|
+
}
|
|
458
|
+
const attachments = messages.attachments;
|
|
459
|
+
if (attachments !== undefined) {
|
|
460
|
+
if (!attachments
|
|
461
|
+
|| typeof attachments !== "object"
|
|
462
|
+
|| Array.isArray(attachments)
|
|
463
|
+
|| attachments.enabled !== true
|
|
464
|
+
|| typeof attachments.bucket !== "string"
|
|
465
|
+
|| !attachments.bucket.trim()) {
|
|
466
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments must enable attachments and name a bucket`);
|
|
467
|
+
}
|
|
468
|
+
validateOptionalPositiveInteger(agent.name, "maxPerMessage", attachments.maxPerMessage);
|
|
469
|
+
validateOptionalPositiveInteger(agent.name, "maxBytesPerFile", attachments.maxBytesPerFile);
|
|
470
|
+
if ((attachments.maxBytesPerFile ?? 0) > storageHardMaxSizeBytes) {
|
|
471
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments.maxBytesPerFile exceeds the storage hard cap`);
|
|
472
|
+
}
|
|
473
|
+
if (attachments.mediaTypes !== undefined) {
|
|
474
|
+
if (!Array.isArray(attachments.mediaTypes)
|
|
475
|
+
|| !attachments.mediaTypes.length
|
|
476
|
+
|| attachments.mediaTypes.some((value) => typeof value !== "string" || !/^image\/[A-Za-z0-9.+-]+$/.test(value))) {
|
|
477
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments.mediaTypes must contain image media types`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const recentMessages = attachments.resolveForModel?.recentMessages;
|
|
481
|
+
if (attachments.resolveForModel !== undefined
|
|
482
|
+
&& (!attachments.resolveForModel
|
|
483
|
+
|| typeof attachments.resolveForModel !== "object"
|
|
484
|
+
|| Array.isArray(attachments.resolveForModel)
|
|
485
|
+
|| (recentMessages !== undefined && (!Number.isInteger(recentMessages) || recentMessages < 0)))) {
|
|
486
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.attachments.resolveForModel.recentMessages must be a non-negative integer`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const voiceNotes = messages.voiceNotes;
|
|
490
|
+
if (voiceNotes !== undefined) {
|
|
491
|
+
if (!voiceNotes
|
|
492
|
+
|| typeof voiceNotes !== "object"
|
|
493
|
+
|| Array.isArray(voiceNotes)
|
|
494
|
+
|| voiceNotes.enabled !== true) {
|
|
495
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.voiceNotes must enable voice notes`);
|
|
496
|
+
}
|
|
497
|
+
if (!attachments?.enabled) {
|
|
498
|
+
throw new Error(`Raybase agent ${agent.name} conversations.messages.voiceNotes requires attachments to be enabled`);
|
|
499
|
+
}
|
|
500
|
+
validateOptionalPositiveInteger(agent.name, "voiceNotes.maxSeconds", voiceNotes.maxSeconds);
|
|
501
|
+
}
|
|
502
|
+
validateAgentUserDataPartsConfig(agent.name, messages.userDataParts);
|
|
503
|
+
}
|
|
504
|
+
function validateOptionalPositiveInteger(agentName, key, value) {
|
|
505
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
|
|
506
|
+
const path = key.startsWith("voiceNotes.") ? key : `attachments.${key}`;
|
|
507
|
+
throw new Error(`Raybase agent ${agentName} conversations.messages.${path} must be a positive integer`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
export function agentConversationAttachmentsConfig(agent) {
|
|
511
|
+
const config = agent.conversations?.messages?.attachments;
|
|
512
|
+
if (!config)
|
|
513
|
+
return null;
|
|
514
|
+
return {
|
|
515
|
+
enabled: true,
|
|
516
|
+
bucket: config.bucket.trim(),
|
|
517
|
+
maxPerMessage: config.maxPerMessage ?? defaultAgentAttachmentMaxPerMessage,
|
|
518
|
+
maxBytesPerFile: config.maxBytesPerFile ?? defaultAgentAttachmentMaxBytesPerFile,
|
|
519
|
+
mediaTypes: [...(config.mediaTypes ?? defaultAgentAttachmentMediaTypes)],
|
|
520
|
+
resolveForModel: {
|
|
521
|
+
recentMessages: config.resolveForModel?.recentMessages ?? defaultAgentAttachmentRecentMessages
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
export function agentConversationVoiceNotesConfig(agent) {
|
|
526
|
+
const attachments = agentConversationAttachmentsConfig(agent);
|
|
527
|
+
if (!attachments)
|
|
528
|
+
return null;
|
|
529
|
+
return normalizeAgentVoiceNotesConfig(agent.conversations?.messages?.voiceNotes, attachments.maxBytesPerFile);
|
|
530
|
+
}
|
|
531
|
+
export function validateAgentAttachmentStorage(agents, storage) {
|
|
532
|
+
for (const agent of Object.values(agents)) {
|
|
533
|
+
const attachments = agentConversationAttachmentsConfig(agent);
|
|
534
|
+
if (!attachments)
|
|
535
|
+
continue;
|
|
536
|
+
if (!storage?.buckets[attachments.bucket]) {
|
|
537
|
+
throw new Error(`Raybase agent ${agent.name} attachment bucket ${attachments.bucket} is not configured in storage`);
|
|
538
|
+
}
|
|
539
|
+
const voiceNotes = agentConversationVoiceNotesConfig(agent);
|
|
540
|
+
const acceptedMediaTypes = storage.buckets[attachments.bucket]?.accept;
|
|
541
|
+
if (voiceNotes && acceptedMediaTypes && !acceptedMediaTypes.includes("audio/wav")) {
|
|
542
|
+
throw new Error(`Raybase agent ${agent.name} voice note bucket ${attachments.bucket} must accept audio/wav`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export function agentConversationEligibilityConfig(agent, conversationKind) {
|
|
547
|
+
const conversations = agent.conversations;
|
|
548
|
+
if (conversationKind !== "room")
|
|
549
|
+
return conversations ?? {};
|
|
550
|
+
return {
|
|
551
|
+
eligibility: agent.rooms?.eligibility ?? conversations?.eligibility,
|
|
552
|
+
eligibilityTimeoutMs: agent.rooms?.eligibilityTimeoutMs ?? conversations?.eligibilityTimeoutMs,
|
|
553
|
+
onEligibilityFailure: agent.rooms?.onEligibilityFailure ?? conversations?.onEligibilityFailure
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function validateNamedAgentHooks(agentName, label, hooks, epochs) {
|
|
557
|
+
if (hooks === undefined)
|
|
558
|
+
return;
|
|
559
|
+
if (!Array.isArray(hooks))
|
|
560
|
+
throw new Error(`Raybase agent ${agentName} ${label}s must be an array`);
|
|
561
|
+
const seen = new Set();
|
|
562
|
+
for (const [index, hook] of hooks.entries()) {
|
|
563
|
+
if (!hook || typeof hook !== "object")
|
|
564
|
+
throw new Error(`Raybase agent ${agentName} ${label} at index ${index} must be an object`);
|
|
565
|
+
validateRaybaseName("agent", hook.name);
|
|
566
|
+
if (seen.has(hook.name))
|
|
567
|
+
throw new Error(`Raybase agent ${agentName} declares duplicate ${label} ${hook.name}`);
|
|
568
|
+
seen.add(hook.name);
|
|
569
|
+
if (typeof hook.render !== "function")
|
|
570
|
+
throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} render must be a function`);
|
|
571
|
+
if (hook.when !== undefined && typeof hook.when !== "function") {
|
|
572
|
+
throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} when must be a function`);
|
|
573
|
+
}
|
|
574
|
+
if (epochs && hook.epoch !== undefined && !epochs.includes(String(hook.epoch))) {
|
|
575
|
+
throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} has invalid epoch ${String(hook.epoch)}`);
|
|
576
|
+
}
|
|
577
|
+
if (hook.maxChars !== undefined && (!Number.isInteger(hook.maxChars) || hook.maxChars < 1)) {
|
|
578
|
+
throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} maxChars must be a positive integer`);
|
|
579
|
+
}
|
|
580
|
+
if (hook.onOverflow !== undefined && hook.onOverflow !== "truncate" && hook.onOverflow !== "error") {
|
|
581
|
+
throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} onOverflow must be truncate or error`);
|
|
582
|
+
}
|
|
583
|
+
if (hook.trust !== undefined && (label !== "sentinel" || hook.trust !== "app" && hook.trust !== "data")) {
|
|
584
|
+
throw new Error(`Raybase agent ${agentName} ${label} ${hook.name} trust must be app or data`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function validateAgentHookTimeout(agentName, label, timeoutMs) {
|
|
589
|
+
if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)) {
|
|
590
|
+
throw new Error(`Raybase agent ${agentName} ${label}.timeoutMs must be between 1 and 60000`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function validateAgentHookFailure(agentName, label, failure) {
|
|
594
|
+
if (failure !== undefined && failure !== "fail-turn" && failure !== "continue-empty") {
|
|
595
|
+
throw new Error(`Raybase agent ${agentName} ${label}.onFailure must be fail-turn or continue-empty`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
export function validateAgentEntriesForAgent(agent) {
|
|
599
|
+
const entries = agent.entries;
|
|
600
|
+
if (entries === undefined)
|
|
601
|
+
return;
|
|
602
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
603
|
+
throw new Error(`Raybase agent ${agent.name} entries must be an object`);
|
|
604
|
+
}
|
|
605
|
+
if (!agent.triggers?.some((trigger) => trigger.type === "chat")) {
|
|
606
|
+
throw new Error(`Raybase agent ${agent.name} declares entries but has no chat trigger`);
|
|
607
|
+
}
|
|
608
|
+
for (const [kind, definition] of Object.entries(entries)) {
|
|
609
|
+
if (!agentEntryKindPattern.test(kind)) {
|
|
610
|
+
throw new Error(`Raybase agent ${agent.name} entry kind "${kind}" must be a lowercase slug`);
|
|
611
|
+
}
|
|
612
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition)) {
|
|
613
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} must be an object`);
|
|
614
|
+
}
|
|
615
|
+
if (typeof definition.description !== "string" || !definition.description.trim()) {
|
|
616
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} requires a description`);
|
|
617
|
+
}
|
|
618
|
+
const source = definition.source ?? "inline";
|
|
619
|
+
if (source !== "inline" && source !== "notification") {
|
|
620
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} has an invalid source`);
|
|
621
|
+
}
|
|
622
|
+
if (definition.payloadSchema !== undefined)
|
|
623
|
+
validateAgentEntrySchema(agent.name, kind, definition.payloadSchema);
|
|
624
|
+
if (definition.userMessage !== undefined) {
|
|
625
|
+
if (typeof definition.userMessage !== "string" || !definition.userMessage.trim()) {
|
|
626
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} has an invalid userMessage`);
|
|
627
|
+
}
|
|
628
|
+
if (definition.userMessage.length > maxAgentEntryKickoffChars) {
|
|
629
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} userMessage exceeds ${maxAgentEntryKickoffChars} characters`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const audiences = definition.audience ?? ["signed-in"];
|
|
633
|
+
if (!Array.isArray(audiences) || audiences.length === 0 || audiences.some((audience) => audience !== "signed-in" && audience !== "public")) {
|
|
634
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} has an invalid audience`);
|
|
635
|
+
}
|
|
636
|
+
if (definition.userMessage && audiences.includes("public")) {
|
|
637
|
+
throw new Error(`Raybase agent ${agent.name} entry ${kind} cannot declare a kickoff userMessage for a public audience`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function validateAgentEntrySchema(agentName, kind, schema) {
|
|
642
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
643
|
+
throw new Error(`Raybase agent ${agentName} entry ${kind} payloadSchema must be a JSON Schema object`);
|
|
644
|
+
}
|
|
645
|
+
const allowedTypes = new Set(["object", "array", "string", "number", "integer", "boolean", "null"]);
|
|
646
|
+
const visit = (node, path) => {
|
|
647
|
+
if (!node || typeof node !== "object" || Array.isArray(node))
|
|
648
|
+
throw new Error(`${path} must be a JSON Schema object`);
|
|
649
|
+
if (node.type !== undefined && (typeof node.type !== "string" || !allowedTypes.has(node.type)))
|
|
650
|
+
throw new Error(`${path}.type is unsupported`);
|
|
651
|
+
if (node.properties !== undefined) {
|
|
652
|
+
if (!node.properties || typeof node.properties !== "object" || Array.isArray(node.properties))
|
|
653
|
+
throw new Error(`${path}.properties must be an object`);
|
|
654
|
+
for (const [key, child] of Object.entries(node.properties))
|
|
655
|
+
visit(child, `${path}.properties.${key}`);
|
|
656
|
+
}
|
|
657
|
+
if (node.items !== undefined)
|
|
658
|
+
visit(node.items, `${path}.items`);
|
|
659
|
+
};
|
|
660
|
+
visit(schema, `Raybase agent ${agentName} entry ${kind} payloadSchema`);
|
|
661
|
+
}
|
|
662
|
+
export function agentEntryDefinition(agent, kind) {
|
|
663
|
+
const definition = agent.entries?.[kind];
|
|
664
|
+
if (!definition)
|
|
665
|
+
throw new AgentHttpError(`Unknown agent entry kind: ${kind}`, 400);
|
|
666
|
+
return definition;
|
|
667
|
+
}
|
|
668
|
+
export function agentEntryAudiences(definition) {
|
|
669
|
+
return uniqueAudiences(definition.audience ?? ["signed-in"]);
|
|
670
|
+
}
|
|
671
|
+
export function validateInlineAgentEntryPayload(definition, payload) {
|
|
672
|
+
let serialized;
|
|
673
|
+
try {
|
|
674
|
+
serialized = stableJson(payload ?? null);
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
throw new AgentHttpError("Agent entry payload must be JSON serializable", 400);
|
|
678
|
+
}
|
|
679
|
+
if (encoder.encode(serialized).byteLength > maxAgentEntryPayloadBytes) {
|
|
680
|
+
throw new AgentHttpError(`Agent entry payload exceeds ${maxAgentEntryPayloadBytes} bytes`, 400);
|
|
681
|
+
}
|
|
682
|
+
if (!definition.payloadSchema)
|
|
683
|
+
return payload;
|
|
684
|
+
try {
|
|
685
|
+
return validateJsonSchema(definition.payloadSchema, payload, "entry.payload");
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
throw new AgentHttpError(error instanceof Error ? error.message : "Agent entry payload is invalid", 400);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
export function agentEntryFromInstance(instance) {
|
|
692
|
+
if (!instance.entryKind || !instance.entryJson)
|
|
693
|
+
return undefined;
|
|
694
|
+
try {
|
|
695
|
+
const parsed = JSON.parse(instance.entryJson);
|
|
696
|
+
if (parsed?.kind !== instance.entryKind || (parsed.source !== "inline" && parsed.source !== "notification"))
|
|
697
|
+
return undefined;
|
|
698
|
+
return parsed;
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function validateAgentConversationTitles(agent) {
|
|
705
|
+
const config = agent.conversationTitles;
|
|
706
|
+
if (config === undefined || config === "auto")
|
|
707
|
+
return;
|
|
708
|
+
if (config && typeof config === "object" && !Array.isArray(config)) {
|
|
709
|
+
if (config.model === undefined || (typeof config.model === "string" && config.model.trim()))
|
|
710
|
+
return;
|
|
711
|
+
throw new Error(`Raybase agent ${agent.name} conversationTitles.model must be a non-empty string`);
|
|
712
|
+
}
|
|
713
|
+
throw new Error(`Raybase agent ${agent.name} conversationTitles must be "auto" or an object like { model }`);
|
|
714
|
+
}
|
|
715
|
+
export function agentConversationTitlesConfig(agent) {
|
|
716
|
+
const config = agent.conversationTitles;
|
|
717
|
+
if (config === undefined)
|
|
718
|
+
return null;
|
|
719
|
+
if (config === "auto")
|
|
720
|
+
return { model: null };
|
|
721
|
+
return { model: config.model?.trim() || null };
|
|
722
|
+
}
|
|
723
|
+
function validateAgentCompaction(compaction) {
|
|
724
|
+
if (compaction === undefined)
|
|
725
|
+
return;
|
|
726
|
+
if (!compaction || typeof compaction !== "object" || Array.isArray(compaction)) {
|
|
727
|
+
throw new Error("Raybase agent compaction must be an object");
|
|
728
|
+
}
|
|
729
|
+
if (compaction.enabled !== undefined && typeof compaction.enabled !== "boolean") {
|
|
730
|
+
throw new Error("Raybase agent compaction enabled must be a boolean");
|
|
731
|
+
}
|
|
732
|
+
if (compaction.model !== undefined && (typeof compaction.model !== "string" || !compaction.model.trim())) {
|
|
733
|
+
throw new Error("Raybase agent compaction model must be a non-empty string");
|
|
734
|
+
}
|
|
735
|
+
if (compaction.keepRecentMessages !== undefined && (!Number.isInteger(compaction.keepRecentMessages) || compaction.keepRecentMessages < 1)) {
|
|
736
|
+
throw new Error("Raybase agent compaction keepRecentMessages must be an integer of at least 1");
|
|
737
|
+
}
|
|
738
|
+
if (compaction.triggerRatio !== undefined && (typeof compaction.triggerRatio !== "number" || !(compaction.triggerRatio > 0) || compaction.triggerRatio > 1)) {
|
|
739
|
+
throw new Error("Raybase agent compaction triggerRatio must be a number greater than 0 and at most 1");
|
|
740
|
+
}
|
|
741
|
+
if (compaction.minSummaryOutputTokens !== undefined && (!Number.isInteger(compaction.minSummaryOutputTokens) || compaction.minSummaryOutputTokens < 1)) {
|
|
742
|
+
throw new Error("Raybase agent compaction minSummaryOutputTokens must be an integer of at least 1");
|
|
743
|
+
}
|
|
744
|
+
if (compaction.summarySpanRatio !== undefined && (typeof compaction.summarySpanRatio !== "number" || !(compaction.summarySpanRatio > 0) || compaction.summarySpanRatio > 1)) {
|
|
745
|
+
throw new Error("Raybase agent compaction summarySpanRatio must be a number greater than 0 and at most 1");
|
|
746
|
+
}
|
|
747
|
+
if (compaction.maxSummaryOutputTokens !== undefined && (!Number.isInteger(compaction.maxSummaryOutputTokens) || compaction.maxSummaryOutputTokens < 1)) {
|
|
748
|
+
throw new Error("Raybase agent compaction maxSummaryOutputTokens must be an integer of at least 1");
|
|
749
|
+
}
|
|
750
|
+
// Only checked when both are pinned: a lone maxSummaryOutputTokens pin below
|
|
751
|
+
// the default floor is the pre-proportional flat budget and must stay valid.
|
|
752
|
+
if (compaction.minSummaryOutputTokens !== undefined
|
|
753
|
+
&& compaction.maxSummaryOutputTokens !== undefined
|
|
754
|
+
&& compaction.minSummaryOutputTokens > compaction.maxSummaryOutputTokens) {
|
|
755
|
+
throw new Error("Raybase agent compaction minSummaryOutputTokens must not exceed maxSummaryOutputTokens");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function validateAgentAllRuntimeToolNames(agent) {
|
|
759
|
+
const seen = new Map();
|
|
760
|
+
const add = (toolName, source) => {
|
|
761
|
+
const existing = seen.get(toolName);
|
|
762
|
+
if (existing)
|
|
763
|
+
throw new Error(`Raybase agent ${agent.name} has duplicate runtime tool name "${toolName}" from ${existing} and ${source}`);
|
|
764
|
+
seen.set(toolName, source);
|
|
765
|
+
};
|
|
766
|
+
// Uniqueness always covers the full registered set; per-turn offer gates
|
|
767
|
+
// never relax it.
|
|
768
|
+
for (const { action } of agentActionRegistrations(agent.actions))
|
|
769
|
+
add(agentActionToolName(action.name), `direct action ${action.name}`);
|
|
770
|
+
for (const binding of agentSkillActionBindings(agent))
|
|
771
|
+
add(binding.modelToolName, `skill ${binding.skill.name} action ${binding.action.name}`);
|
|
772
|
+
const skillRegistrations = agentSkillRegistrations(agent.skills, agent.name);
|
|
773
|
+
if (skillRegistrations.some(({ policy, mode, skill }) => Boolean(skill.guidance) && (Boolean(policy) || mode !== "include" && mode !== "excluded")) || agent.skillStore?.enabled === true) {
|
|
774
|
+
add("load_agent_skill", "built-in skill loader");
|
|
775
|
+
}
|
|
776
|
+
if (skillRegistrations.some(({ policy, mode, skill }) => (Boolean(policy) || mode !== "excluded") && (Boolean(skill.files?.length) || Boolean(skill.guidance && skill.guidance.length > (skill.maxChars ?? (skill.ephemeral ? 4_000 : 8_000))))) || agent.skillStore?.enabled === true) {
|
|
777
|
+
add("read_skill", "built-in skill reader");
|
|
778
|
+
}
|
|
779
|
+
for (const binding of agentRemoteMcpToolBindings(agent))
|
|
780
|
+
add(binding.modelToolName, `remote MCP ${binding.server.name}/${binding.toolName}`);
|
|
781
|
+
if (agentMemorySpaces(agent).length) {
|
|
782
|
+
for (const toolName of raybaseAgentMemoryToolNames)
|
|
783
|
+
add(toolName, "built-in memory tool");
|
|
784
|
+
}
|
|
785
|
+
if (agent.skillStore?.enabled === true) {
|
|
786
|
+
const access = agent.skillStore.agentAccess ?? {};
|
|
787
|
+
for (const name of raybaseAgentSkillToolNames) {
|
|
788
|
+
const mutating = name === "skill_write" ? access.create : name === "skill_edit" ? access.edit : name === "skill_delete" ? access.delete : name === "skill_set_enabled" ? access.setEnabled : name.startsWith("skill_import_") ? Boolean(access.importSources?.length) : true;
|
|
789
|
+
if (mutating)
|
|
790
|
+
add(name, "built-in database skill tool");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (agent.wakes?.enabled === true) {
|
|
794
|
+
for (const toolName of raybaseAgentWakeToolNames)
|
|
795
|
+
add(toolName, "built-in wake tool");
|
|
796
|
+
}
|
|
797
|
+
if (agent.followUps?.enabled === true) {
|
|
798
|
+
for (const toolName of ["follow_up", "list_follow_ups", "close_follow_up"])
|
|
799
|
+
add(toolName, "built-in follow-up tool");
|
|
800
|
+
}
|
|
801
|
+
if (agent.conversations?.messages?.reactions?.agentTool === true) {
|
|
802
|
+
add("react", "built-in conversation reaction tool");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function normalizeAgentTriggers(triggers, maintenanceRuns = []) {
|
|
806
|
+
const declared = triggers?.length ? triggers : [{ type: "chat" }];
|
|
807
|
+
const existingMaintenance = new Set();
|
|
808
|
+
for (const [index, trigger] of declared.entries()) {
|
|
809
|
+
if (trigger?.type === "schedule" && trigger.maintenance !== undefined) {
|
|
810
|
+
const run = maintenanceRuns.find((candidate) => candidate.name === trigger.maintenance);
|
|
811
|
+
if (!run
|
|
812
|
+
|| trigger.cron !== run.schedule.cron
|
|
813
|
+
|| (trigger.timezone ?? undefined) !== run.schedule.timezone
|
|
814
|
+
|| trigger.userMessage !== run.instruction) {
|
|
815
|
+
throw new Error(`Raybase agent schedule trigger at index ${index} cannot declare maintenance directly; use memory.maintenance`);
|
|
816
|
+
}
|
|
817
|
+
existingMaintenance.add(trigger.maintenance);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const normalized = [
|
|
821
|
+
...declared,
|
|
822
|
+
...maintenanceRuns.filter((run) => !existingMaintenance.has(run.name)).map((run) => ({
|
|
823
|
+
type: "schedule",
|
|
824
|
+
cron: run.schedule.cron,
|
|
825
|
+
...(run.schedule.timezone ? { timezone: run.schedule.timezone } : {}),
|
|
826
|
+
userMessage: run.instruction,
|
|
827
|
+
maintenance: run.name
|
|
828
|
+
}))
|
|
829
|
+
];
|
|
830
|
+
const seen = new Set();
|
|
831
|
+
return normalized.map((trigger, index) => {
|
|
832
|
+
if (!trigger || typeof trigger !== "object")
|
|
833
|
+
throw new Error(`Raybase agent trigger at index ${index} must be an object`);
|
|
834
|
+
let key;
|
|
835
|
+
if (trigger.type === "chat") {
|
|
836
|
+
key = "chat";
|
|
837
|
+
}
|
|
838
|
+
else if (trigger.type === "schedule") {
|
|
839
|
+
if (typeof trigger.cron !== "string" || !trigger.cron.trim())
|
|
840
|
+
throw new Error(`Raybase agent schedule trigger at index ${index} requires cron`);
|
|
841
|
+
validateCron(trigger.cron);
|
|
842
|
+
if (trigger.timezone !== undefined && typeof trigger.timezone !== "string")
|
|
843
|
+
throw new Error(`Raybase agent schedule trigger at index ${index} has invalid timezone`);
|
|
844
|
+
if (trigger.userMessage !== undefined) {
|
|
845
|
+
if (typeof trigger.userMessage !== "string" || !trigger.userMessage.trim()) {
|
|
846
|
+
throw new Error(`Raybase agent schedule trigger at index ${index} has invalid userMessage`);
|
|
847
|
+
}
|
|
848
|
+
trigger = { ...trigger, userMessage: trigger.userMessage.trim() };
|
|
849
|
+
}
|
|
850
|
+
key = trigger.maintenance
|
|
851
|
+
? `maintenance:${trigger.maintenance}`
|
|
852
|
+
: `schedule:${trigger.cron}:${trigger.timezone ?? "UTC"}:${stableJson(trigger.input ?? null)}`;
|
|
853
|
+
}
|
|
854
|
+
else if (trigger.type === "data") {
|
|
855
|
+
if (typeof trigger.event !== "string" || !dataTriggerEventPattern.test(trigger.event)) {
|
|
856
|
+
throw new Error(`Raybase agent data trigger at index ${index} must use table.create, table.update, or table.delete`);
|
|
857
|
+
}
|
|
858
|
+
key = `data:${trigger.event}:${trigger.includeAdmin === true}:${stableJson(trigger.input ?? null)}`;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
throw new Error(`Unsupported Raybase agent trigger type: ${String(trigger.type)}`);
|
|
862
|
+
}
|
|
863
|
+
if (seen.has(key))
|
|
864
|
+
throw new Error(`Duplicate Raybase agent trigger: ${key}`);
|
|
865
|
+
seen.add(key);
|
|
866
|
+
return trigger;
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
export function validateAgentTriggers(agents, schema) {
|
|
870
|
+
const tableNames = new Set(Object.values(schema.tables).map((table) => table.name));
|
|
871
|
+
for (const agent of Object.values(defineAgents(agents ?? {}))) {
|
|
872
|
+
agent.triggers.forEach((trigger, index) => {
|
|
873
|
+
if (trigger.type !== "data")
|
|
874
|
+
return;
|
|
875
|
+
const parsed = parseAgentDataTriggerEvent(trigger.event);
|
|
876
|
+
if (!tableNames.has(parsed.tableName)) {
|
|
877
|
+
throw new Error(`Raybase agent ${agent.name} data trigger ${index} references unknown table ${parsed.tableName}`);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
export function parseAgentDataTriggerEvent(event) {
|
|
883
|
+
const match = event.match(dataTriggerEventPattern);
|
|
884
|
+
if (!match)
|
|
885
|
+
throw new Error(`Invalid Raybase agent data trigger event: ${event}`);
|
|
886
|
+
return { tableName: match[1], op: match[2] };
|
|
887
|
+
}
|
|
888
|
+
export function defineAgents(agents) {
|
|
889
|
+
const seen = new Set();
|
|
890
|
+
const normalized = {};
|
|
891
|
+
for (const [key, agent] of Object.entries(agents)) {
|
|
892
|
+
const name = agent.name || key;
|
|
893
|
+
validateAgentName(name);
|
|
894
|
+
if (seen.has(name))
|
|
895
|
+
throw new Error(`Duplicate Raybase agent name: ${name}`);
|
|
896
|
+
seen.add(name);
|
|
897
|
+
normalized[key] = defineAgent({ ...agent, name });
|
|
898
|
+
}
|
|
899
|
+
return normalized;
|
|
900
|
+
}
|
|
901
|
+
export function validateAgentName(name) {
|
|
902
|
+
if (!agentNamePattern.test(name)) {
|
|
903
|
+
throw new Error(`Invalid Raybase agent name "${name}". Use letters, numbers, "_", "-", ".", or ":".`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
export function agentSummaries(agents) {
|
|
907
|
+
return Object.values(defineAgents(agents ?? {})).map((agent) => ({
|
|
908
|
+
name: agent.name,
|
|
909
|
+
title: agent.title ?? agent.name,
|
|
910
|
+
description: agent.description ?? "",
|
|
911
|
+
runtime: agent.runtime,
|
|
912
|
+
triggers: agent.triggers,
|
|
913
|
+
model: agent.model ?? null,
|
|
914
|
+
limits: agent.limits ?? null,
|
|
915
|
+
scopes: agent.scopes ?? [],
|
|
916
|
+
owner: agent.owner ?? null,
|
|
917
|
+
actions: agentActionRegistrations(agent.actions).map(({ action }) => ({ name: action.name, description: action.description ?? "" })),
|
|
918
|
+
skills: agentSkillSummaries(agent.skills),
|
|
919
|
+
skillStore: agent.skillStore ?? null,
|
|
920
|
+
remoteMcpBindings: agentRemoteMcpBindingSummaries([agent]),
|
|
921
|
+
memorySpaces: agentMemorySpaces(agent),
|
|
922
|
+
rooms: agent.rooms?.enabled === true,
|
|
923
|
+
entries: Object.entries(agent.entries ?? {}).map(([kind, definition]) => ({
|
|
924
|
+
kind,
|
|
925
|
+
description: definition.description,
|
|
926
|
+
source: definition.source ?? "inline",
|
|
927
|
+
audience: agentEntryAudiences(definition),
|
|
928
|
+
hasKickoff: Boolean(definition.userMessage),
|
|
929
|
+
payloadSchema: definition.payloadSchema ?? null
|
|
930
|
+
})),
|
|
931
|
+
runtimeControl: {
|
|
932
|
+
configurable: agent.runtimeControl?.configurable === true,
|
|
933
|
+
defaultEnabled: agent.runtimeControl?.defaultEnabled !== false
|
|
934
|
+
},
|
|
935
|
+
prompt: agent.prompt
|
|
936
|
+
? {
|
|
937
|
+
configurable: agent.prompt.configurable === true,
|
|
938
|
+
renderer: agent.prompt.renderer ?? "liquid",
|
|
939
|
+
default: agent.prompt.default
|
|
940
|
+
}
|
|
941
|
+
: null
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
export function agentFor(agents, name) {
|
|
945
|
+
const registry = defineAgents(agents ?? {});
|
|
946
|
+
const agent = Object.values(registry).find((candidate) => candidate.name === name || candidate.name.toLowerCase() === name.toLowerCase());
|
|
947
|
+
if (!agent)
|
|
948
|
+
throw new AgentHttpError(`Unknown agent: ${name}`, 404);
|
|
949
|
+
return agent;
|
|
950
|
+
}
|
|
951
|
+
export class AgentHttpError extends Error {
|
|
952
|
+
status;
|
|
953
|
+
retryable;
|
|
954
|
+
constructor(message, status, retryable = false) {
|
|
955
|
+
super(message);
|
|
956
|
+
this.status = status;
|
|
957
|
+
this.retryable = retryable;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
export function agentAudienceKinds(audience, surface) {
|
|
961
|
+
const configured = audience ?? "signed-in";
|
|
962
|
+
if (Array.isArray(configured))
|
|
963
|
+
return uniqueAudiences(configured);
|
|
964
|
+
if (typeof configured === "string")
|
|
965
|
+
return [configured];
|
|
966
|
+
const value = configured[surface] ?? configured.default ?? configured.app ?? "signed-in";
|
|
967
|
+
return Array.isArray(value) ? uniqueAudiences(value) : [value];
|
|
968
|
+
}
|
|
969
|
+
export function agentAudienceAllows(audience, surface, kind) {
|
|
970
|
+
return agentAudienceKinds(audience, surface).includes(kind);
|
|
971
|
+
}
|
|
972
|
+
export function agentRequestSurface(request, surfaces = []) {
|
|
973
|
+
const url = new URL(request.url);
|
|
974
|
+
const referer = request.headers.get("referer");
|
|
975
|
+
const requestedRoute = request.headers.get("x-raybase-agent-route");
|
|
976
|
+
const requestedSurface = request.headers.get("x-raybase-agent-surface");
|
|
977
|
+
let route = url.pathname || "/";
|
|
978
|
+
let origin = request.headers.get("origin") ?? undefined;
|
|
979
|
+
if (requestedRoute?.trim()) {
|
|
980
|
+
route = requestedRoute.trim();
|
|
981
|
+
}
|
|
982
|
+
else if (referer) {
|
|
983
|
+
try {
|
|
984
|
+
const ref = new URL(referer);
|
|
985
|
+
if (ref.origin === url.origin) {
|
|
986
|
+
route = ref.pathname || "/";
|
|
987
|
+
origin = ref.origin;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
// Keep the API route when Referer is malformed.
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const normalizedRoute = normalizeRoute(route);
|
|
995
|
+
const inferredSurface = surfaces.find((entry) => {
|
|
996
|
+
const prefix = normalizeRoute(entry.pathPrefix);
|
|
997
|
+
return normalizedRoute === prefix || normalizedRoute.startsWith(`${prefix.replace(/\/$/, "")}/`);
|
|
998
|
+
})?.surface ?? "app";
|
|
999
|
+
const surface = requestedSurface?.trim() || inferredSurface;
|
|
1000
|
+
return { surface, route: normalizedRoute, origin };
|
|
1001
|
+
}
|
|
1002
|
+
export async function resolveAgentAccess(input) {
|
|
1003
|
+
const requestContext = agentRequestSurface(input.request, input.surfaces);
|
|
1004
|
+
const allowed = agentAudienceKinds(input.agent.audience, requestContext.surface);
|
|
1005
|
+
const requestedKind = input.principalKind;
|
|
1006
|
+
const organizationId = input.organizationId ?? principalOrganizationId(input.user);
|
|
1007
|
+
if (requestedKind === "user" || (!requestedKind && allowed.includes("signed-in") && input.user)) {
|
|
1008
|
+
if (!allowed.includes("signed-in"))
|
|
1009
|
+
throw new AgentHttpError("Agent surface does not allow signed-in access", 403);
|
|
1010
|
+
if (!input.user)
|
|
1011
|
+
throw new AgentHttpError("Authentication required", 401);
|
|
1012
|
+
return {
|
|
1013
|
+
audience: "signed-in",
|
|
1014
|
+
principal: {
|
|
1015
|
+
kind: "user",
|
|
1016
|
+
user: input.user,
|
|
1017
|
+
userId: input.user.id,
|
|
1018
|
+
membershipId: optionalUserField(input.user, "membershipId"),
|
|
1019
|
+
organizationId: organizationId ?? undefined,
|
|
1020
|
+
roles: input.user.appRoles
|
|
1021
|
+
},
|
|
1022
|
+
surface: requestContext.surface,
|
|
1023
|
+
route: requestContext.route,
|
|
1024
|
+
origin: requestContext.origin,
|
|
1025
|
+
organizationId
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (requestedKind === "public" || (!requestedKind && allowed.includes("public"))) {
|
|
1029
|
+
if (!allowed.includes("public"))
|
|
1030
|
+
throw new AgentHttpError("Agent surface does not allow public access", 403);
|
|
1031
|
+
return {
|
|
1032
|
+
audience: "public",
|
|
1033
|
+
principal: await publicAgentPrincipal(input.request, organizationId, requestContext),
|
|
1034
|
+
surface: requestContext.surface,
|
|
1035
|
+
route: requestContext.route,
|
|
1036
|
+
origin: requestContext.origin,
|
|
1037
|
+
organizationId
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
throw new AgentHttpError("Authentication required", 401);
|
|
1041
|
+
}
|
|
1042
|
+
export function publicAgentUser(principal) {
|
|
1043
|
+
return {
|
|
1044
|
+
id: `public:${principal.id}`,
|
|
1045
|
+
email: "public@raybase.local",
|
|
1046
|
+
name: "Public visitor",
|
|
1047
|
+
operatorRole: null,
|
|
1048
|
+
appRoles: []
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
export function agentInstanceKey(agentName, conversationId) {
|
|
1052
|
+
return `raybase-agent:${agentName}:${conversationId}`;
|
|
1053
|
+
}
|
|
1054
|
+
export function agentExpiresAt(ttlSeconds = defaultConversationTtlSeconds) {
|
|
1055
|
+
if (ttlSeconds === null)
|
|
1056
|
+
return null;
|
|
1057
|
+
return new Date(wallNowMs() + ttlSeconds * 1000).toISOString();
|
|
1058
|
+
}
|
|
1059
|
+
export async function createOrResumeAgentInstance(input) {
|
|
1060
|
+
const now = nowIso();
|
|
1061
|
+
const retention = normalizeAgentRetentionConfig(input.agent.retention);
|
|
1062
|
+
const isUserPrincipal = input.access.principal.kind === "user";
|
|
1063
|
+
const keepHot = isUserPrincipal && retention.transcripts === "keep-hot";
|
|
1064
|
+
const defaultTtlSeconds = isUserPrincipal ? retention.hotTtlMs / 1000 : retention.publicHotTtlMs / 1000;
|
|
1065
|
+
const expiresAt = agentExpiresAt(keepHot ? null : input.ttlSeconds ?? defaultTtlSeconds);
|
|
1066
|
+
if (input.conversationId) {
|
|
1067
|
+
const existing = await findAgentInstance(input.control, input.agent.name, input.conversationId);
|
|
1068
|
+
if (!existing)
|
|
1069
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1070
|
+
if (existing.retentionReason === "user-deleted")
|
|
1071
|
+
throw new AgentHttpError("Agent conversation has been deleted", 410);
|
|
1072
|
+
await assertAgentConversationAccess({ control: input.control, instance: existing, access: input.access });
|
|
1073
|
+
if (existing.status === "deleted")
|
|
1074
|
+
throw new AgentHttpError("Agent conversation has been deleted", 410);
|
|
1075
|
+
if (existing.status === "deleting" || existing.status === "cleanupRequested") {
|
|
1076
|
+
throw new AgentHttpError("Agent conversation cleanup is in progress", 409);
|
|
1077
|
+
}
|
|
1078
|
+
if (existing.status === "cleanupError") {
|
|
1079
|
+
throw new AgentHttpError("Agent conversation cleanup must complete before it can resume", 503);
|
|
1080
|
+
}
|
|
1081
|
+
if (input.entry && stableJson(input.entry) !== (existing.entryJson ?? "")) {
|
|
1082
|
+
throw new AgentHttpError("Agent conversation entry context conflicts with the existing snapshot", 409);
|
|
1083
|
+
}
|
|
1084
|
+
const metadataJson = agentInstanceMetadataJson({
|
|
1085
|
+
current: existing.metadataJson,
|
|
1086
|
+
metadata: input.metadata,
|
|
1087
|
+
title: input.title
|
|
1088
|
+
});
|
|
1089
|
+
if (existing.status === "archived") {
|
|
1090
|
+
const lastChapter = await loadLastAgentChapter(input.control, existing.conversationId);
|
|
1091
|
+
if (!lastChapter) {
|
|
1092
|
+
throw new AgentHttpError("Archived conversation context is unavailable; retry later", 503);
|
|
1093
|
+
}
|
|
1094
|
+
if (!input.rehydrateArchived) {
|
|
1095
|
+
throw new AgentHttpError("Agent conversation rehydration is unavailable", 503);
|
|
1096
|
+
}
|
|
1097
|
+
const synopsis = buildAgentChapterRehydrateSeed(lastChapter);
|
|
1098
|
+
try {
|
|
1099
|
+
await input.rehydrateArchived(existing, synopsis);
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
throw new AgentHttpError(`Agent conversation rehydration failed: ${error instanceof Error ? error.message : String(error)}`, 503);
|
|
1103
|
+
}
|
|
1104
|
+
await input.control.run(`UPDATE ${q("_agent_instances")}
|
|
1105
|
+
SET "status" = 'active', "cleanupRequestedAt" = NULL, "deletedAt" = NULL, "retentionReason" = NULL,
|
|
1106
|
+
"updatedAt" = ?, "lastActiveAt" = ?, "expiresAt" = ?, "route" = ?, "surface" = ?,
|
|
1107
|
+
"metadataJson" = ?, "lastError" = NULL
|
|
1108
|
+
WHERE "id" = ? AND "status" = 'archived'`, [
|
|
1109
|
+
now,
|
|
1110
|
+
now,
|
|
1111
|
+
expiresAt,
|
|
1112
|
+
input.access.route,
|
|
1113
|
+
input.access.surface,
|
|
1114
|
+
metadataJson,
|
|
1115
|
+
existing.id
|
|
1116
|
+
]);
|
|
1117
|
+
const activated = await findAgentInstance(input.control, input.agent.name, input.conversationId);
|
|
1118
|
+
if (activated?.status === "active")
|
|
1119
|
+
return activated;
|
|
1120
|
+
if (activated?.status === "archived") {
|
|
1121
|
+
throw new AgentHttpError("Agent conversation rehydration activation did not complete", 503);
|
|
1122
|
+
}
|
|
1123
|
+
throw new AgentHttpError("Agent conversation changed while rehydrating", 409);
|
|
1124
|
+
}
|
|
1125
|
+
await input.control.run(`UPDATE ${q("_agent_instances")}
|
|
1126
|
+
SET "status" = 'active', "cleanupRequestedAt" = NULL, "deletedAt" = NULL, "updatedAt" = ?, "lastActiveAt" = ?,
|
|
1127
|
+
"expiresAt" = ?, "route" = ?, "surface" = ?, "metadataJson" = ?, "lastError" = NULL
|
|
1128
|
+
WHERE "id" = ? AND "status" = 'active'
|
|
1129
|
+
AND COALESCE("retentionReason", '') <> 'user-deleted'`, [
|
|
1130
|
+
now,
|
|
1131
|
+
now,
|
|
1132
|
+
expiresAt,
|
|
1133
|
+
input.access.route,
|
|
1134
|
+
input.access.surface,
|
|
1135
|
+
metadataJson,
|
|
1136
|
+
existing.id
|
|
1137
|
+
]);
|
|
1138
|
+
const refreshed = await findAgentInstance(input.control, input.agent.name, input.conversationId);
|
|
1139
|
+
if (!refreshed)
|
|
1140
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1141
|
+
if (refreshed.retentionReason === "user-deleted" || refreshed.status === "deleted") {
|
|
1142
|
+
throw new AgentHttpError("Agent conversation has been deleted", 410);
|
|
1143
|
+
}
|
|
1144
|
+
if (refreshed.status === "cleanupRequested" || refreshed.status === "deleting" || refreshed.status === "archived") {
|
|
1145
|
+
throw new AgentHttpError("Agent conversation changed while resuming; retry", 409);
|
|
1146
|
+
}
|
|
1147
|
+
if (refreshed.status === "cleanupError") {
|
|
1148
|
+
throw new AgentHttpError("Agent conversation cleanup must complete before it can resume", 503);
|
|
1149
|
+
}
|
|
1150
|
+
return refreshed;
|
|
1151
|
+
}
|
|
1152
|
+
const conversationId = randomId("agent_conv");
|
|
1153
|
+
const idValue = randomId("agent_inst");
|
|
1154
|
+
const row = {
|
|
1155
|
+
id: idValue,
|
|
1156
|
+
conversationId,
|
|
1157
|
+
agentName: input.agent.name,
|
|
1158
|
+
agentTitle: input.agent.title ?? input.agent.name,
|
|
1159
|
+
agentOwner: input.agent.owner?.packageName ?? input.agent.owner?.moduleId ?? null,
|
|
1160
|
+
moduleId: input.agent.owner?.moduleId ?? null,
|
|
1161
|
+
runtime: input.agent.runtime,
|
|
1162
|
+
triggerType: "chat",
|
|
1163
|
+
instanceKey: agentInstanceKey(input.agent.name, conversationId),
|
|
1164
|
+
principalKind: input.access.principal.kind,
|
|
1165
|
+
userId: input.access.principal.kind === "user" ? input.access.principal.user.id : null,
|
|
1166
|
+
publicScopeHash: input.access.principal.kind === "public" ? input.access.principal.id : null,
|
|
1167
|
+
organizationId: input.access.organizationId ?? null,
|
|
1168
|
+
route: input.access.route,
|
|
1169
|
+
surface: input.access.surface,
|
|
1170
|
+
status: "active",
|
|
1171
|
+
cleanupRequestedAt: null,
|
|
1172
|
+
hotStorageDeletedAt: null,
|
|
1173
|
+
deletedAt: null,
|
|
1174
|
+
retentionReason: null,
|
|
1175
|
+
pinnedAt: null,
|
|
1176
|
+
createdAt: now,
|
|
1177
|
+
updatedAt: now,
|
|
1178
|
+
lastActiveAt: now,
|
|
1179
|
+
expiresAt,
|
|
1180
|
+
lastRunId: null,
|
|
1181
|
+
lastError: null,
|
|
1182
|
+
metadataJson: agentInstanceMetadataJson({
|
|
1183
|
+
metadata: input.metadata,
|
|
1184
|
+
title: input.title
|
|
1185
|
+
}),
|
|
1186
|
+
entryKind: input.entry?.kind ?? null,
|
|
1187
|
+
entryJson: input.entry ? stableJson(input.entry) : null,
|
|
1188
|
+
kind: null,
|
|
1189
|
+
roomMembershipRev: null
|
|
1190
|
+
};
|
|
1191
|
+
await input.control.run(`INSERT INTO ${q("_agent_instances")}
|
|
1192
|
+
("id", "conversationId", "agentName", "agentTitle", "agentOwner", "moduleId", "runtime", "triggerType", "instanceKey",
|
|
1193
|
+
"principalKind", "userId", "publicScopeHash", "organizationId", "route", "surface", "status", "cleanupRequestedAt",
|
|
1194
|
+
"hotStorageDeletedAt", "deletedAt", "retentionReason", "pinnedAt", "createdAt", "updatedAt", "lastActiveAt", "expiresAt", "lastRunId", "lastError", "metadataJson", "entryKind", "entryJson", "kind", "roomMembershipRev")
|
|
1195
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1196
|
+
row.id,
|
|
1197
|
+
row.conversationId,
|
|
1198
|
+
row.agentName,
|
|
1199
|
+
row.agentTitle,
|
|
1200
|
+
row.agentOwner,
|
|
1201
|
+
row.moduleId,
|
|
1202
|
+
row.runtime,
|
|
1203
|
+
row.triggerType,
|
|
1204
|
+
row.instanceKey,
|
|
1205
|
+
row.principalKind,
|
|
1206
|
+
row.userId,
|
|
1207
|
+
row.publicScopeHash,
|
|
1208
|
+
row.organizationId,
|
|
1209
|
+
row.route,
|
|
1210
|
+
row.surface,
|
|
1211
|
+
row.status,
|
|
1212
|
+
row.cleanupRequestedAt,
|
|
1213
|
+
row.hotStorageDeletedAt,
|
|
1214
|
+
row.deletedAt,
|
|
1215
|
+
row.retentionReason,
|
|
1216
|
+
row.pinnedAt,
|
|
1217
|
+
row.createdAt,
|
|
1218
|
+
row.updatedAt,
|
|
1219
|
+
row.lastActiveAt,
|
|
1220
|
+
row.expiresAt,
|
|
1221
|
+
row.lastRunId,
|
|
1222
|
+
row.lastError,
|
|
1223
|
+
row.metadataJson,
|
|
1224
|
+
row.entryKind,
|
|
1225
|
+
row.entryJson,
|
|
1226
|
+
row.kind ?? null,
|
|
1227
|
+
row.roomMembershipRev ?? null
|
|
1228
|
+
]);
|
|
1229
|
+
return row;
|
|
1230
|
+
}
|
|
1231
|
+
export function buildAgentRoomInstance(input) {
|
|
1232
|
+
if (!input.agent.rooms?.enabled)
|
|
1233
|
+
throw new AgentHttpError(`Agent ${input.agent.name} does not enable rooms`, 400);
|
|
1234
|
+
if (!input.agent.triggers.some((trigger) => trigger.type === "chat")) {
|
|
1235
|
+
throw new AgentHttpError(`Agent ${input.agent.name} does not declare a chat trigger`, 400);
|
|
1236
|
+
}
|
|
1237
|
+
const now = input.now ?? nowIso();
|
|
1238
|
+
const retention = normalizeAgentRetentionConfig(input.agent.retention);
|
|
1239
|
+
const expiresAt = agentExpiresAt(retention.transcripts === "keep-hot" ? null : retention.hotTtlMs / 1000);
|
|
1240
|
+
const row = {
|
|
1241
|
+
id: randomId("agent_inst"),
|
|
1242
|
+
conversationId: input.conversationId,
|
|
1243
|
+
agentName: input.agent.name,
|
|
1244
|
+
agentTitle: input.agent.title ?? input.agent.name,
|
|
1245
|
+
agentOwner: input.agent.owner?.packageName ?? input.agent.owner?.moduleId ?? null,
|
|
1246
|
+
moduleId: input.agent.owner?.moduleId ?? null,
|
|
1247
|
+
runtime: input.agent.runtime,
|
|
1248
|
+
triggerType: "chat",
|
|
1249
|
+
instanceKey: agentInstanceKey(input.agent.name, input.conversationId),
|
|
1250
|
+
principalKind: "user",
|
|
1251
|
+
userId: input.ownerUserId,
|
|
1252
|
+
publicScopeHash: null,
|
|
1253
|
+
organizationId: input.organizationId ?? null,
|
|
1254
|
+
route: input.route ?? null,
|
|
1255
|
+
surface: input.surface ?? "app",
|
|
1256
|
+
status: "active",
|
|
1257
|
+
cleanupRequestedAt: null,
|
|
1258
|
+
hotStorageDeletedAt: null,
|
|
1259
|
+
deletedAt: null,
|
|
1260
|
+
retentionReason: null,
|
|
1261
|
+
pinnedAt: null,
|
|
1262
|
+
createdAt: now,
|
|
1263
|
+
updatedAt: now,
|
|
1264
|
+
lastActiveAt: now,
|
|
1265
|
+
expiresAt,
|
|
1266
|
+
lastRunId: null,
|
|
1267
|
+
lastError: null,
|
|
1268
|
+
metadataJson: agentInstanceMetadataJson({ title: input.title }),
|
|
1269
|
+
entryKind: null,
|
|
1270
|
+
entryJson: null,
|
|
1271
|
+
kind: "room",
|
|
1272
|
+
roomMembershipRev: 1
|
|
1273
|
+
};
|
|
1274
|
+
return row;
|
|
1275
|
+
}
|
|
1276
|
+
export function agentRoomInstanceInsertStatement(row) {
|
|
1277
|
+
return {
|
|
1278
|
+
sql: `INSERT INTO ${q("_agent_instances")}
|
|
1279
|
+
("id", "conversationId", "agentName", "agentTitle", "agentOwner", "moduleId", "runtime", "triggerType", "instanceKey",
|
|
1280
|
+
"principalKind", "userId", "publicScopeHash", "organizationId", "route", "surface", "status", "cleanupRequestedAt",
|
|
1281
|
+
"hotStorageDeletedAt", "deletedAt", "retentionReason", "pinnedAt", "createdAt", "updatedAt", "lastActiveAt", "expiresAt", "lastRunId", "lastError", "metadataJson", "entryKind", "entryJson", "kind", "roomMembershipRev")
|
|
1282
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1283
|
+
params: [
|
|
1284
|
+
row.id, row.conversationId, row.agentName, row.agentTitle, row.agentOwner, row.moduleId,
|
|
1285
|
+
row.runtime, row.triggerType, row.instanceKey, row.principalKind, row.userId, row.publicScopeHash,
|
|
1286
|
+
row.organizationId, row.route, row.surface, row.status, row.cleanupRequestedAt, row.hotStorageDeletedAt,
|
|
1287
|
+
row.deletedAt, row.retentionReason, row.pinnedAt, row.createdAt, row.updatedAt, row.lastActiveAt,
|
|
1288
|
+
row.expiresAt, row.lastRunId, row.lastError, row.metadataJson, row.entryKind, row.entryJson, row.kind ?? null,
|
|
1289
|
+
row.roomMembershipRev ?? null
|
|
1290
|
+
]
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
export async function createAgentRoomInstance(input) {
|
|
1294
|
+
const row = buildAgentRoomInstance(input);
|
|
1295
|
+
const statement = agentRoomInstanceInsertStatement(row);
|
|
1296
|
+
await input.control.run(statement.sql, statement.params);
|
|
1297
|
+
return row;
|
|
1298
|
+
}
|
|
1299
|
+
export async function findAgentInstance(control, agentName, conversationId) {
|
|
1300
|
+
return control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "agentName" = ? AND "conversationId" = ? LIMIT 1`, [agentName, conversationId]);
|
|
1301
|
+
}
|
|
1302
|
+
export async function listAgentConversations(control, input) {
|
|
1303
|
+
const limit = boundedAgentConversationPage(input.limit, 20, 1, 100);
|
|
1304
|
+
const offset = boundedAgentConversationPage(input.offset, 0, 0, 10_000);
|
|
1305
|
+
const now = input.now ?? nowIso();
|
|
1306
|
+
const membershipUserId = input.access.principal.kind === "user" ? input.access.principal.user.id : "";
|
|
1307
|
+
const commonParams = [membershipUserId, input.agentName, now];
|
|
1308
|
+
const principalColumn = input.access.principal.kind === "user" ? "userId" : "publicScopeHash";
|
|
1309
|
+
const principalId = input.access.principal.kind === "user" ? input.access.principal.user.id : input.access.principal.id;
|
|
1310
|
+
const query = input.q?.trim();
|
|
1311
|
+
const searchQuery = query ? parseSearchQuery(query) : null;
|
|
1312
|
+
const titleQuery = searchQuery?.terms.join(" ") || query;
|
|
1313
|
+
const hasNormalizedTitleQuery = Boolean(query && titleQuery && titleQuery !== query);
|
|
1314
|
+
if (searchQuery)
|
|
1315
|
+
await ensureAgentChapterSearch(control);
|
|
1316
|
+
const statusPredicate = input.includeArchived && input.access.principal.kind === "user"
|
|
1317
|
+
? `i."status" IN ('active', 'archived')`
|
|
1318
|
+
: `i."status" = 'active'`;
|
|
1319
|
+
const searchPredicate = query
|
|
1320
|
+
? `AND (
|
|
1321
|
+
LOWER(COALESCE(json_extract(i."metadataJson", '$.title'), '')) LIKE LOWER(?)
|
|
1322
|
+
${hasNormalizedTitleQuery
|
|
1323
|
+
? `OR LOWER(COALESCE(json_extract(i."metadataJson", '$.title'), '')) LIKE LOWER(?)`
|
|
1324
|
+
: ""}
|
|
1325
|
+
${searchQuery ? `OR EXISTS (
|
|
1326
|
+
SELECT 1 FROM ${q("_agent_chapter_fts")} f
|
|
1327
|
+
JOIN ${q("_agent_chapters")} c ON c."id" = f."chapterId"
|
|
1328
|
+
WHERE f."conversationId" = i."conversationId"
|
|
1329
|
+
AND c."agentName" = i."agentName"
|
|
1330
|
+
AND c."userId" = i."userId"
|
|
1331
|
+
AND ${q("_agent_chapter_fts")} MATCH ?
|
|
1332
|
+
)` : ""}
|
|
1333
|
+
)`
|
|
1334
|
+
: "";
|
|
1335
|
+
const rows = await control.all(`SELECT i.*, r."status" AS "lastRunStatus", m."pinnedAt" AS "roomMemberPinnedAt", m."lastReadAt" AS "roomMemberLastReadAt"
|
|
1336
|
+
FROM ${q("_agent_instances")} i
|
|
1337
|
+
LEFT JOIN ${q("_agent_runs")} r ON r."runId" = i."lastRunId"
|
|
1338
|
+
LEFT JOIN ${q("_agent_room_members")} m
|
|
1339
|
+
ON i."kind" = 'room' AND m."conversationId" = i."conversationId" AND m."userId" = ?
|
|
1340
|
+
WHERE i."agentName" = ?
|
|
1341
|
+
AND i."runtime" = 'ai-chat'
|
|
1342
|
+
AND i."triggerType" = 'chat'
|
|
1343
|
+
AND ${statusPredicate}
|
|
1344
|
+
AND (i."status" = 'archived' OR i."expiresAt" IS NULL OR i."expiresAt" > ?)
|
|
1345
|
+
AND i."surface" = ?
|
|
1346
|
+
AND COALESCE(i."organizationId", '') = COALESCE(?, '')
|
|
1347
|
+
AND (
|
|
1348
|
+
(COALESCE(i."kind", '') <> 'room' AND i."principalKind" = ? AND i.${q(principalColumn)} = ?)
|
|
1349
|
+
OR (i."kind" = 'room' AND ? = 'user' AND m."status" = 'active')
|
|
1350
|
+
)
|
|
1351
|
+
${searchPredicate}
|
|
1352
|
+
ORDER BY CASE WHEN COALESCE(m."pinnedAt", i."pinnedAt") IS NULL THEN 1 ELSE 0 END,
|
|
1353
|
+
COALESCE(m."pinnedAt", i."pinnedAt") DESC, i."lastActiveAt" DESC, i."createdAt" DESC
|
|
1354
|
+
LIMIT ? OFFSET ?`, [
|
|
1355
|
+
...commonParams,
|
|
1356
|
+
input.access.surface,
|
|
1357
|
+
input.access.organizationId ?? null,
|
|
1358
|
+
input.access.principal.kind,
|
|
1359
|
+
principalId,
|
|
1360
|
+
input.access.principal.kind,
|
|
1361
|
+
...(query
|
|
1362
|
+
? [
|
|
1363
|
+
`%${query}%`,
|
|
1364
|
+
...(hasNormalizedTitleQuery ? [`%${titleQuery}%`] : []),
|
|
1365
|
+
...(searchQuery ? [searchQuery.match] : [])
|
|
1366
|
+
]
|
|
1367
|
+
: []),
|
|
1368
|
+
limit,
|
|
1369
|
+
offset
|
|
1370
|
+
]);
|
|
1371
|
+
return rows.map((row) => agentConversationSummaryFromRow({
|
|
1372
|
+
...row,
|
|
1373
|
+
pinnedAt: row.kind === "room" ? row.roomMemberPinnedAt : row.pinnedAt,
|
|
1374
|
+
lastReadAt: row.kind === "room" ? row.roomMemberLastReadAt : row.lastReadAt ?? null
|
|
1375
|
+
}));
|
|
1376
|
+
}
|
|
1377
|
+
export async function loadAgentConversationForAccess(control, input) {
|
|
1378
|
+
const instance = await loadAgentInstance(control, input.agentName, input.conversationId);
|
|
1379
|
+
await assertAgentConversationAccess({ control, instance, access: input.access });
|
|
1380
|
+
return instance;
|
|
1381
|
+
}
|
|
1382
|
+
export async function requestAgentConversationDeletion(control, input) {
|
|
1383
|
+
const instance = await findAgentInstance(control, input.agentName, input.conversationId);
|
|
1384
|
+
if (!instance)
|
|
1385
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1386
|
+
await assertAgentConversationAccess({ control, instance, access: input.access, require: "owner" });
|
|
1387
|
+
const now = input.now ?? nowIso();
|
|
1388
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
1389
|
+
SET "status" = CASE
|
|
1390
|
+
WHEN "status" = 'deleted' THEN 'deleted'
|
|
1391
|
+
WHEN "status" = 'deleting' THEN 'deleting'
|
|
1392
|
+
ELSE 'cleanupRequested'
|
|
1393
|
+
END,
|
|
1394
|
+
"cleanupRequestedAt" = CASE
|
|
1395
|
+
WHEN "status" = 'deleted' THEN "cleanupRequestedAt"
|
|
1396
|
+
ELSE COALESCE("cleanupRequestedAt", ?)
|
|
1397
|
+
END,
|
|
1398
|
+
"deletedAt" = CASE
|
|
1399
|
+
WHEN "status" = 'deleted' THEN COALESCE("deletedAt", ?)
|
|
1400
|
+
ELSE "deletedAt"
|
|
1401
|
+
END,
|
|
1402
|
+
"retentionReason" = 'user-deleted',
|
|
1403
|
+
"expiresAt" = ?,
|
|
1404
|
+
"updatedAt" = ?,
|
|
1405
|
+
"lastError" = NULL
|
|
1406
|
+
WHERE "id" = ?`, [now, now, now, now, instance.id]);
|
|
1407
|
+
const claimed = await findAgentInstance(control, input.agentName, input.conversationId);
|
|
1408
|
+
if (!claimed)
|
|
1409
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1410
|
+
if (claimed.retentionReason !== "user-deleted") {
|
|
1411
|
+
throw new AgentHttpError("Agent conversation deletion could not be secured", 409);
|
|
1412
|
+
}
|
|
1413
|
+
if (claimed.status !== "deleted") {
|
|
1414
|
+
try {
|
|
1415
|
+
await input.deleteHotStorage(claimed);
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
throw new AgentHttpError(`Agent conversation hot-storage deletion failed: ${error instanceof Error ? error.message : String(error)}`, 503);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const completed = await findAgentInstance(control, input.agentName, input.conversationId);
|
|
1422
|
+
if (!completed)
|
|
1423
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1424
|
+
if (completed.status !== "deleted" || completed.retentionReason !== "user-deleted") {
|
|
1425
|
+
throw new AgentHttpError("Agent conversation deletion has not completed", 503);
|
|
1426
|
+
}
|
|
1427
|
+
await purgeAgentChaptersForConversation(control, {
|
|
1428
|
+
agentName: completed.agentName,
|
|
1429
|
+
conversationId: completed.conversationId
|
|
1430
|
+
});
|
|
1431
|
+
return { agentName: instance.agentName, conversationId: instance.conversationId, status: "deleted" };
|
|
1432
|
+
}
|
|
1433
|
+
export async function loadAgentInstance(control, agentName, conversationId) {
|
|
1434
|
+
const row = await findAgentInstance(control, agentName, conversationId);
|
|
1435
|
+
if (!row)
|
|
1436
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1437
|
+
if (row.status !== "active")
|
|
1438
|
+
throw new AgentHttpError("Agent conversation is not active", 410);
|
|
1439
|
+
if (row.expiresAt && new Date(row.expiresAt).getTime() <= wallNowMs())
|
|
1440
|
+
throw new AgentHttpError("Agent conversation expired", 410);
|
|
1441
|
+
return row;
|
|
1442
|
+
}
|
|
1443
|
+
function agentConversationSummaryFromRow(row) {
|
|
1444
|
+
return {
|
|
1445
|
+
agentName: row.agentName,
|
|
1446
|
+
conversationId: row.conversationId,
|
|
1447
|
+
runtime: row.runtime,
|
|
1448
|
+
triggerType: "chat",
|
|
1449
|
+
title: agentConversationTitle(row.metadataJson),
|
|
1450
|
+
agentTitle: row.agentTitle,
|
|
1451
|
+
route: row.route,
|
|
1452
|
+
surface: row.surface,
|
|
1453
|
+
status: row.status,
|
|
1454
|
+
createdAt: row.createdAt,
|
|
1455
|
+
updatedAt: row.updatedAt,
|
|
1456
|
+
lastActiveAt: row.lastActiveAt,
|
|
1457
|
+
expiresAt: row.expiresAt,
|
|
1458
|
+
pinnedAt: row.pinnedAt,
|
|
1459
|
+
archived: row.status === "archived",
|
|
1460
|
+
lastRunId: row.lastRunId,
|
|
1461
|
+
lastRunStatus: row.lastRunStatus ?? null,
|
|
1462
|
+
entryKind: row.entryKind,
|
|
1463
|
+
kind: row.kind ?? null,
|
|
1464
|
+
lastReadAt: row.lastReadAt ?? null
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
// Title semantics: undefined keeps the stored title, null clears it, and a
|
|
1468
|
+
// string replaces it (a string that cleans to nothing keeps the stored title
|
|
1469
|
+
// rather than silently clearing). Resuming a conversation without a title
|
|
1470
|
+
// must never wipe one that a client or the runtime already stored.
|
|
1471
|
+
export function agentInstanceMetadataJson(input) {
|
|
1472
|
+
if (input.metadata === undefined && input.title === undefined)
|
|
1473
|
+
return input.current ?? null;
|
|
1474
|
+
const currentTitle = cleanAgentConversationTitle(parseAgentMetadata(input.current).title);
|
|
1475
|
+
const metadata = input.metadata === undefined
|
|
1476
|
+
? parseAgentMetadata(input.current)
|
|
1477
|
+
: normalizeAgentMetadata(input.metadata);
|
|
1478
|
+
const title = input.title === null
|
|
1479
|
+
? undefined
|
|
1480
|
+
: input.title !== undefined
|
|
1481
|
+
? cleanAgentConversationTitle(input.title) ?? currentTitle
|
|
1482
|
+
: currentTitle ?? cleanAgentConversationTitle(metadata.title);
|
|
1483
|
+
delete metadata.title;
|
|
1484
|
+
if (title)
|
|
1485
|
+
metadata.title = title;
|
|
1486
|
+
return Object.keys(metadata).length ? stableJson(metadata) : null;
|
|
1487
|
+
}
|
|
1488
|
+
export function agentConversationTitle(metadataJson) {
|
|
1489
|
+
const title = cleanAgentConversationTitle(parseAgentMetadata(metadataJson).title);
|
|
1490
|
+
return title ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
export async function updateAgentConversationTitle(control, instance, title, now) {
|
|
1493
|
+
const metadataJson = agentInstanceMetadataJson({ current: instance.metadataJson, title });
|
|
1494
|
+
await control.run(`UPDATE ${q("_agent_instances")} SET "metadataJson" = ?, "updatedAt" = ? WHERE "id" = ?`, [metadataJson, now ?? nowIso(), instance.id]);
|
|
1495
|
+
return agentConversationTitle(metadataJson);
|
|
1496
|
+
}
|
|
1497
|
+
export async function updateAgentConversationPinned(control, instance, pinned, now) {
|
|
1498
|
+
const timestamp = now ?? nowIso();
|
|
1499
|
+
const pinnedAt = pinned ? timestamp : null;
|
|
1500
|
+
await control.run(`UPDATE ${q("_agent_instances")} SET "pinnedAt" = ?, "updatedAt" = ? WHERE "id" = ?`, [pinnedAt, timestamp, instance.id]);
|
|
1501
|
+
return pinnedAt;
|
|
1502
|
+
}
|
|
1503
|
+
export async function markAgentDirectConversationRead(control, instance, now) {
|
|
1504
|
+
if (instance.kind === "room")
|
|
1505
|
+
throw new AgentHttpError("Agent conversation is a room", 400);
|
|
1506
|
+
const lastReadAt = now ?? nowIso();
|
|
1507
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
1508
|
+
SET "lastReadAt" = CASE
|
|
1509
|
+
WHEN "lastReadAt" IS NULL OR "lastReadAt" < ? THEN ?
|
|
1510
|
+
ELSE "lastReadAt"
|
|
1511
|
+
END,
|
|
1512
|
+
"updatedAt" = CASE
|
|
1513
|
+
WHEN "lastReadAt" IS NULL OR "lastReadAt" < ? THEN ?
|
|
1514
|
+
ELSE "updatedAt"
|
|
1515
|
+
END
|
|
1516
|
+
WHERE "id" = ?`, [lastReadAt, lastReadAt, lastReadAt, lastReadAt, instance.id]);
|
|
1517
|
+
const row = await control.first(`SELECT "lastReadAt" FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [instance.id]);
|
|
1518
|
+
if (!row?.lastReadAt)
|
|
1519
|
+
throw new AgentHttpError("Agent conversation read cursor could not be advanced", 500);
|
|
1520
|
+
return row.lastReadAt;
|
|
1521
|
+
}
|
|
1522
|
+
// Dedicated rename path: unlike the conversations POST resume, this never
|
|
1523
|
+
// flips status back to active, bumps lastActiveAt, or extends expiresAt.
|
|
1524
|
+
export async function renameAgentConversation(control, input) {
|
|
1525
|
+
if (input.title !== null && typeof input.title !== "string") {
|
|
1526
|
+
throw new AgentHttpError("Conversation title must be a string, or null to clear it", 400);
|
|
1527
|
+
}
|
|
1528
|
+
if (typeof input.title === "string" && !cleanAgentConversationTitle(input.title)) {
|
|
1529
|
+
throw new AgentHttpError("Conversation title must not be blank", 400);
|
|
1530
|
+
}
|
|
1531
|
+
const instance = await findAgentInstance(control, input.agentName, input.conversationId);
|
|
1532
|
+
if (!instance)
|
|
1533
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1534
|
+
await assertAgentConversationAccess({ control, instance, access: input.access, require: "owner" });
|
|
1535
|
+
if (instance.status !== "active" && instance.status !== "archived") {
|
|
1536
|
+
throw new AgentHttpError("Agent conversation cannot be renamed in its current state", 409);
|
|
1537
|
+
}
|
|
1538
|
+
const title = await updateAgentConversationTitle(control, instance, input.title, input.now);
|
|
1539
|
+
return { agentName: instance.agentName, conversationId: instance.conversationId, title };
|
|
1540
|
+
}
|
|
1541
|
+
export async function pinAgentConversation(control, input) {
|
|
1542
|
+
if (typeof input.pinned !== "boolean")
|
|
1543
|
+
throw new AgentHttpError("Conversation pinned must be a boolean", 400);
|
|
1544
|
+
if (input.access.principal.kind !== "user")
|
|
1545
|
+
throw new AgentHttpError("Public conversations cannot be pinned", 403);
|
|
1546
|
+
const instance = await findAgentInstance(control, input.agentName, input.conversationId);
|
|
1547
|
+
if (!instance)
|
|
1548
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
1549
|
+
const accessResult = await assertAgentConversationAccess({ control, instance, access: input.access });
|
|
1550
|
+
if (instance.status !== "active" && instance.status !== "archived") {
|
|
1551
|
+
throw new AgentHttpError("Agent conversation cannot be pinned in its current state", 409);
|
|
1552
|
+
}
|
|
1553
|
+
const pinnedAt = instance.kind === "room"
|
|
1554
|
+
? await setAgentRoomMemberPinned({
|
|
1555
|
+
control,
|
|
1556
|
+
conversationId: instance.conversationId,
|
|
1557
|
+
userId: accessResult.membership.userId,
|
|
1558
|
+
pinned: input.pinned,
|
|
1559
|
+
now: input.now
|
|
1560
|
+
})
|
|
1561
|
+
: await updateAgentConversationPinned(control, instance, input.pinned, input.now);
|
|
1562
|
+
return {
|
|
1563
|
+
agentName: instance.agentName,
|
|
1564
|
+
conversationId: instance.conversationId,
|
|
1565
|
+
pinned: Boolean(pinnedAt),
|
|
1566
|
+
pinnedAt
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function parseAgentMetadata(value) {
|
|
1570
|
+
if (!value)
|
|
1571
|
+
return {};
|
|
1572
|
+
try {
|
|
1573
|
+
return normalizeAgentMetadata(JSON.parse(value));
|
|
1574
|
+
}
|
|
1575
|
+
catch {
|
|
1576
|
+
return {};
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function normalizeAgentMetadata(value) {
|
|
1580
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
1581
|
+
}
|
|
1582
|
+
export function cleanAgentConversationTitle(value) {
|
|
1583
|
+
if (typeof value !== "string")
|
|
1584
|
+
return undefined;
|
|
1585
|
+
const title = value.trim().replace(/\s+/g, " ");
|
|
1586
|
+
return title ? title.slice(0, 120) : undefined;
|
|
1587
|
+
}
|
|
1588
|
+
function boundedAgentConversationPage(value, fallback, min, max) {
|
|
1589
|
+
const numeric = Number(value ?? fallback);
|
|
1590
|
+
if (!Number.isFinite(numeric))
|
|
1591
|
+
return fallback;
|
|
1592
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
1593
|
+
}
|
|
1594
|
+
export async function createAgentRun(input) {
|
|
1595
|
+
const now = nowIso();
|
|
1596
|
+
const expiresAt = input.ttlSeconds === undefined
|
|
1597
|
+
? input.instance.expiresAt === null ? null : agentExpiresAt()
|
|
1598
|
+
: agentExpiresAt(input.ttlSeconds);
|
|
1599
|
+
const run = {
|
|
1600
|
+
runId: input.runId || randomId("agent_run"),
|
|
1601
|
+
conversationId: input.instance.conversationId,
|
|
1602
|
+
agentName: input.instance.agentName,
|
|
1603
|
+
requestId: input.requestId,
|
|
1604
|
+
triggerType: input.triggerType ?? input.instance.triggerType,
|
|
1605
|
+
inputMessageId: input.inputMessageId ?? null,
|
|
1606
|
+
status: "queued",
|
|
1607
|
+
startedAt: now,
|
|
1608
|
+
heartbeatAt: now,
|
|
1609
|
+
finishedAt: null,
|
|
1610
|
+
error: null,
|
|
1611
|
+
usageEventId: null,
|
|
1612
|
+
auditEventId: null,
|
|
1613
|
+
metadataJson: input.metadata === undefined ? null : stableJson(input.metadata)
|
|
1614
|
+
};
|
|
1615
|
+
await input.control.run(`INSERT INTO ${q("_agent_runs")}
|
|
1616
|
+
("runId", "conversationId", "agentName", "requestId", "triggerType", "inputMessageId", "status", "startedAt", "heartbeatAt", "finishedAt",
|
|
1617
|
+
"error", "usageEventId", "auditEventId", "metadataJson")
|
|
1618
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1619
|
+
run.runId,
|
|
1620
|
+
run.conversationId,
|
|
1621
|
+
run.agentName,
|
|
1622
|
+
run.requestId,
|
|
1623
|
+
run.triggerType,
|
|
1624
|
+
run.inputMessageId,
|
|
1625
|
+
run.status,
|
|
1626
|
+
run.startedAt,
|
|
1627
|
+
run.heartbeatAt,
|
|
1628
|
+
run.finishedAt,
|
|
1629
|
+
run.error,
|
|
1630
|
+
run.usageEventId,
|
|
1631
|
+
run.auditEventId,
|
|
1632
|
+
run.metadataJson
|
|
1633
|
+
]);
|
|
1634
|
+
await input.control.run(`UPDATE ${q("_agent_instances")}
|
|
1635
|
+
SET "lastRunId" = ?, "lastActiveAt" = ?, "expiresAt" = ?, "updatedAt" = ?, "lastError" = NULL
|
|
1636
|
+
WHERE "id" = ?`, [run.runId, now, expiresAt, now, input.instance.id]);
|
|
1637
|
+
return run;
|
|
1638
|
+
}
|
|
1639
|
+
export function agentEnabledSettingKey(agentName) {
|
|
1640
|
+
return `raybase.agent.${agentName}.enabled`;
|
|
1641
|
+
}
|
|
1642
|
+
export function agentPromptSettingKey(agentName) {
|
|
1643
|
+
return `raybase.agent.${agentName}.prompt`;
|
|
1644
|
+
}
|
|
1645
|
+
export const raybaseAgentTimezoneSettingKey = "raybase.agent.timezone";
|
|
1646
|
+
export function agentGeneratedSettingDefinitions(agents) {
|
|
1647
|
+
const definitions = {};
|
|
1648
|
+
const resolvedAgents = Object.values(defineAgents(agents ?? {}));
|
|
1649
|
+
if (resolvedAgents.length > 0) {
|
|
1650
|
+
definitions[raybaseAgentTimezoneSettingKey] = setting.string({
|
|
1651
|
+
scope: "user",
|
|
1652
|
+
group: "Agents",
|
|
1653
|
+
label: "Timezone",
|
|
1654
|
+
description: "IANA timezone used for per-turn Agent time context.",
|
|
1655
|
+
default: "UTC",
|
|
1656
|
+
validate: (value) => {
|
|
1657
|
+
try {
|
|
1658
|
+
new Intl.DateTimeFormat("en", { timeZone: value }).format();
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
return "Timezone must be a valid IANA timezone such as Europe/Paris.";
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
for (const agent of resolvedAgents) {
|
|
1668
|
+
const group = `Agents / ${agent.title ?? agent.name}`;
|
|
1669
|
+
if (agent.runtimeControl?.configurable === true) {
|
|
1670
|
+
definitions[agentEnabledSettingKey(agent.name)] = setting.boolean({
|
|
1671
|
+
scope: "app",
|
|
1672
|
+
group,
|
|
1673
|
+
label: "Enabled",
|
|
1674
|
+
description: "Controls admission for new chat, scheduled, data-triggered, direct, and wake runs. In-flight work is not cancelled.",
|
|
1675
|
+
default: agent.runtimeControl.defaultEnabled !== false,
|
|
1676
|
+
admin: { hidden: true }
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
if (agent.prompt?.configurable === true) {
|
|
1680
|
+
definitions[agentPromptSettingKey(agent.name)] = setting.string({
|
|
1681
|
+
scope: "app",
|
|
1682
|
+
group,
|
|
1683
|
+
label: "Prompt",
|
|
1684
|
+
description: "Operator override for this agent prompt. Resetting removes the stored override and returns to the code-defined default.",
|
|
1685
|
+
default: agent.prompt.default,
|
|
1686
|
+
admin: { hidden: true, multiline: true },
|
|
1687
|
+
validate: (value) => value.trim() ? true : "Prompt override must not be blank"
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return definitions;
|
|
1692
|
+
}
|
|
1693
|
+
export async function isAgentRuntimeEnabled(settings, agent) {
|
|
1694
|
+
if (agent.runtimeControl?.configurable !== true)
|
|
1695
|
+
return agent.runtimeControl?.defaultEnabled !== false;
|
|
1696
|
+
try {
|
|
1697
|
+
return Boolean(await settings?.get(agentEnabledSettingKey(agent.name)) ?? (agent.runtimeControl.defaultEnabled !== false));
|
|
1698
|
+
}
|
|
1699
|
+
catch {
|
|
1700
|
+
return agent.runtimeControl.defaultEnabled !== false;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
export function agentScheduleId(agentName, triggerIndex, trigger) {
|
|
1704
|
+
if (trigger?.maintenance)
|
|
1705
|
+
return `static:${agentName}:maintenance:${trigger.maintenance}`;
|
|
1706
|
+
return `static:${agentName}:${triggerIndex}`;
|
|
1707
|
+
}
|
|
1708
|
+
export function agentTriggerKey(agentName, triggerIndex, trigger) {
|
|
1709
|
+
if (trigger.type === "chat")
|
|
1710
|
+
return `chat:${agentName}`;
|
|
1711
|
+
if (trigger.type === "schedule") {
|
|
1712
|
+
return trigger.maintenance
|
|
1713
|
+
? `schedule:${agentName}:maintenance:${trigger.maintenance}`
|
|
1714
|
+
: `schedule:${agentName}:${triggerIndex}:${trigger.cron}:${trigger.timezone ?? "UTC"}`;
|
|
1715
|
+
}
|
|
1716
|
+
return `data:${agentName}:${triggerIndex}:${trigger.event}`;
|
|
1717
|
+
}
|
|
1718
|
+
function hasOwn(object, key) {
|
|
1719
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
1720
|
+
}
|
|
1721
|
+
function normalizeScheduleUserMessage(value, label = "Schedule user message") {
|
|
1722
|
+
if (value === undefined || value === null)
|
|
1723
|
+
return null;
|
|
1724
|
+
if (typeof value !== "string")
|
|
1725
|
+
throw new Error(`${label} must be a string`);
|
|
1726
|
+
const trimmed = value.trim();
|
|
1727
|
+
if (!trimmed)
|
|
1728
|
+
throw new Error(`${label} must not be blank`);
|
|
1729
|
+
return trimmed;
|
|
1730
|
+
}
|
|
1731
|
+
function normalizeScheduleTimezoneOverride(value) {
|
|
1732
|
+
if (value === undefined || value === null)
|
|
1733
|
+
return null;
|
|
1734
|
+
if (typeof value !== "string")
|
|
1735
|
+
throw new Error("Schedule timezone override must be a string");
|
|
1736
|
+
const trimmed = value.trim();
|
|
1737
|
+
return trimmed ? trimmed : null;
|
|
1738
|
+
}
|
|
1739
|
+
function normalizeScheduleCronOverride(value) {
|
|
1740
|
+
if (typeof value !== "string" || !value.trim())
|
|
1741
|
+
throw new Error("Schedule cron override must be a non-empty string");
|
|
1742
|
+
const cron = value.trim();
|
|
1743
|
+
validateCron(cron);
|
|
1744
|
+
return cron;
|
|
1745
|
+
}
|
|
1746
|
+
function serializeScheduleJson(value) {
|
|
1747
|
+
const serialized = JSON.stringify(value);
|
|
1748
|
+
if (serialized === undefined)
|
|
1749
|
+
throw new Error("Schedule input override must be JSON-serializable");
|
|
1750
|
+
return stableJson(JSON.parse(serialized));
|
|
1751
|
+
}
|
|
1752
|
+
function parseScheduleJson(value, fallback) {
|
|
1753
|
+
if (value === null || value === undefined)
|
|
1754
|
+
return fallback;
|
|
1755
|
+
try {
|
|
1756
|
+
return JSON.parse(value);
|
|
1757
|
+
}
|
|
1758
|
+
catch {
|
|
1759
|
+
return fallback;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function normalizeOverrideEnabled(value) {
|
|
1763
|
+
return value === 0 || value === 1 ? value : null;
|
|
1764
|
+
}
|
|
1765
|
+
export function parseAgentScheduleSourceInput(row) {
|
|
1766
|
+
return parseScheduleJson(row.inputJson, {});
|
|
1767
|
+
}
|
|
1768
|
+
export function parseAgentScheduleInput(row) {
|
|
1769
|
+
if (row.inputJsonOverrideEnabled === 1)
|
|
1770
|
+
return parseScheduleJson(row.inputJsonOverride, null);
|
|
1771
|
+
return parseAgentScheduleSourceInput(row);
|
|
1772
|
+
}
|
|
1773
|
+
export function agentScheduleOverrideState(row) {
|
|
1774
|
+
return {
|
|
1775
|
+
cron: row.cronOverride !== null && row.cronOverride !== undefined,
|
|
1776
|
+
timezone: row.timezoneOverride !== null && row.timezoneOverride !== undefined,
|
|
1777
|
+
enabled: normalizeOverrideEnabled(row.enabledOverride) !== null,
|
|
1778
|
+
input: row.inputJsonOverrideEnabled === 1,
|
|
1779
|
+
userMessage: row.userMessageOverride !== null && row.userMessageOverride !== undefined
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
export function effectiveAgentSchedule(row) {
|
|
1783
|
+
const enabledOverride = normalizeOverrideEnabled(row.enabledOverride);
|
|
1784
|
+
const userMessage = row.userMessageOverride ?? row.userMessage ?? null;
|
|
1785
|
+
return {
|
|
1786
|
+
id: row.id,
|
|
1787
|
+
agentName: row.agentName,
|
|
1788
|
+
triggerIndex: row.triggerIndex,
|
|
1789
|
+
triggerKey: row.triggerKey,
|
|
1790
|
+
cron: row.cronOverride ?? row.cron,
|
|
1791
|
+
timezone: row.timezoneOverride ?? row.timezone ?? null,
|
|
1792
|
+
enabled: (enabledOverride ?? row.enabled) === 1,
|
|
1793
|
+
input: parseAgentScheduleInput(row),
|
|
1794
|
+
userMessage: userMessage?.trim() || undefined
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
export function agentScheduleRuntimeTrigger(row) {
|
|
1798
|
+
const effective = effectiveAgentSchedule(row);
|
|
1799
|
+
return {
|
|
1800
|
+
type: "schedule",
|
|
1801
|
+
cron: effective.cron,
|
|
1802
|
+
timezone: effective.timezone ?? undefined,
|
|
1803
|
+
input: effective.input,
|
|
1804
|
+
enabled: effective.enabled,
|
|
1805
|
+
userMessage: effective.userMessage
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
export function agentScheduleAdminSnapshot(row) {
|
|
1809
|
+
const overrideEnabled = agentScheduleOverrideState(row);
|
|
1810
|
+
const effective = effectiveAgentSchedule(row);
|
|
1811
|
+
const sourceInput = parseAgentScheduleSourceInput(row);
|
|
1812
|
+
return {
|
|
1813
|
+
id: row.id,
|
|
1814
|
+
agentName: row.agentName,
|
|
1815
|
+
triggerIndex: row.triggerIndex,
|
|
1816
|
+
triggerKey: row.triggerKey,
|
|
1817
|
+
source: {
|
|
1818
|
+
cron: row.cron,
|
|
1819
|
+
timezone: row.timezone ?? null,
|
|
1820
|
+
enabled: row.enabled === 1,
|
|
1821
|
+
input: sourceInput,
|
|
1822
|
+
userMessage: row.userMessage ?? null
|
|
1823
|
+
},
|
|
1824
|
+
overrides: {
|
|
1825
|
+
cron: overrideEnabled.cron ? row.cronOverride : null,
|
|
1826
|
+
timezone: overrideEnabled.timezone ? row.timezoneOverride : null,
|
|
1827
|
+
enabled: overrideEnabled.enabled ? normalizeOverrideEnabled(row.enabledOverride) === 1 : null,
|
|
1828
|
+
input: overrideEnabled.input ? parseScheduleJson(row.inputJsonOverride, null) : null,
|
|
1829
|
+
userMessage: overrideEnabled.userMessage ? row.userMessageOverride : null
|
|
1830
|
+
},
|
|
1831
|
+
overrideEnabled,
|
|
1832
|
+
effective: {
|
|
1833
|
+
cron: effective.cron,
|
|
1834
|
+
timezone: effective.timezone,
|
|
1835
|
+
enabled: effective.enabled,
|
|
1836
|
+
input: effective.input,
|
|
1837
|
+
userMessage: effective.userMessage ?? null
|
|
1838
|
+
},
|
|
1839
|
+
lastRunAt: row.lastRunAt,
|
|
1840
|
+
nextRunAt: row.nextRunAt,
|
|
1841
|
+
service: row.service,
|
|
1842
|
+
createdAt: row.createdAt,
|
|
1843
|
+
updatedAt: row.updatedAt
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
export function scheduleNextRunFrom(now, cron, timezone) {
|
|
1847
|
+
validateCron(cron);
|
|
1848
|
+
return nextCronRun(cron, now, timezone ?? undefined);
|
|
1849
|
+
}
|
|
1850
|
+
function nextRunAfterScheduleChange(input) {
|
|
1851
|
+
if (!input.before)
|
|
1852
|
+
return input.fallbackNextRunAt ?? scheduleNextRunFrom(input.now, input.after.cron, input.after.timezone);
|
|
1853
|
+
const timingChanged = input.before.cron !== input.after.cron || (input.before.timezone ?? null) !== (input.after.timezone ?? null);
|
|
1854
|
+
const reenabled = !input.before.enabled && input.after.enabled;
|
|
1855
|
+
if (timingChanged || reenabled)
|
|
1856
|
+
return scheduleNextRunFrom(input.now, input.after.cron, input.after.timezone);
|
|
1857
|
+
return input.currentNextRunAt;
|
|
1858
|
+
}
|
|
1859
|
+
async function findAgentSchedule(control, idValue) {
|
|
1860
|
+
return control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ? LIMIT 1`, [idValue]);
|
|
1861
|
+
}
|
|
1862
|
+
export async function loadAgentSchedule(control, idValue) {
|
|
1863
|
+
const row = await findAgentSchedule(control, idValue);
|
|
1864
|
+
if (!row)
|
|
1865
|
+
throw new Error(`Agent schedule not found: ${idValue}`);
|
|
1866
|
+
return row;
|
|
1867
|
+
}
|
|
1868
|
+
export async function listAgentSchedulesForAgent(control, agentName) {
|
|
1869
|
+
return control.all(`SELECT * FROM ${q("_agent_schedules")} WHERE "agentName" = ? ORDER BY "triggerIndex" ASC`, [agentName]);
|
|
1870
|
+
}
|
|
1871
|
+
function applyReset(row, field) {
|
|
1872
|
+
if (field === "cron")
|
|
1873
|
+
row.cronOverride = null;
|
|
1874
|
+
if (field === "timezone")
|
|
1875
|
+
row.timezoneOverride = null;
|
|
1876
|
+
if (field === "enabled")
|
|
1877
|
+
row.enabledOverride = null;
|
|
1878
|
+
if (field === "input") {
|
|
1879
|
+
row.inputJsonOverride = null;
|
|
1880
|
+
row.inputJsonOverrideEnabled = null;
|
|
1881
|
+
}
|
|
1882
|
+
if (field === "userMessage")
|
|
1883
|
+
row.userMessageOverride = null;
|
|
1884
|
+
}
|
|
1885
|
+
function normalizeResetFields(fields) {
|
|
1886
|
+
if (fields === undefined || fields === null)
|
|
1887
|
+
return [];
|
|
1888
|
+
if (!Array.isArray(fields))
|
|
1889
|
+
throw new Error("Schedule reset must be an array");
|
|
1890
|
+
const allowed = new Set(["cron", "timezone", "enabled", "input", "userMessage"]);
|
|
1891
|
+
return fields.map((field) => {
|
|
1892
|
+
if (typeof field !== "string" || !allowed.has(field))
|
|
1893
|
+
throw new Error(`Unknown schedule override field: ${String(field)}`);
|
|
1894
|
+
return field;
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
async function persistScheduleOverrideRow(control, row) {
|
|
1898
|
+
await control.run(`UPDATE ${q("_agent_schedules")}
|
|
1899
|
+
SET "cronOverride" = ?, "timezoneOverride" = ?, "enabledOverride" = ?,
|
|
1900
|
+
"inputJsonOverride" = ?, "inputJsonOverrideEnabled" = ?, "userMessageOverride" = ?,
|
|
1901
|
+
"nextRunAt" = ?, "dispatchGeneration" = ?, "dispatchAttempts" = ?,
|
|
1902
|
+
"dispatchLeaseExpiresAt" = ?, "lastDispatchAt" = ?, "lastDispatchError" = ?, "updatedAt" = ?
|
|
1903
|
+
WHERE "id" = ?`, [
|
|
1904
|
+
row.cronOverride,
|
|
1905
|
+
row.timezoneOverride,
|
|
1906
|
+
row.enabledOverride,
|
|
1907
|
+
row.inputJsonOverride,
|
|
1908
|
+
row.inputJsonOverrideEnabled,
|
|
1909
|
+
row.userMessageOverride,
|
|
1910
|
+
row.nextRunAt,
|
|
1911
|
+
row.dispatchGeneration,
|
|
1912
|
+
row.dispatchAttempts,
|
|
1913
|
+
row.dispatchLeaseExpiresAt,
|
|
1914
|
+
row.lastDispatchAt,
|
|
1915
|
+
row.lastDispatchError,
|
|
1916
|
+
row.updatedAt,
|
|
1917
|
+
row.id
|
|
1918
|
+
]);
|
|
1919
|
+
return loadAgentSchedule(control, row.id);
|
|
1920
|
+
}
|
|
1921
|
+
export async function setAgentScheduleOverrides(control, input) {
|
|
1922
|
+
const current = await loadAgentSchedule(control, input.id);
|
|
1923
|
+
const beforeEffective = effectiveAgentSchedule(current);
|
|
1924
|
+
const before = agentScheduleAdminSnapshot(current);
|
|
1925
|
+
const next = { ...current };
|
|
1926
|
+
const changedFields = new Set();
|
|
1927
|
+
for (const field of normalizeResetFields(input.reset)) {
|
|
1928
|
+
applyReset(next, field);
|
|
1929
|
+
changedFields.add(field);
|
|
1930
|
+
}
|
|
1931
|
+
const overrides = input.overrides && typeof input.overrides === "object" && !Array.isArray(input.overrides)
|
|
1932
|
+
? input.overrides
|
|
1933
|
+
: {};
|
|
1934
|
+
const allowed = new Set(["cron", "timezone", "enabled", "input", "userMessage"]);
|
|
1935
|
+
for (const key of Object.keys(overrides)) {
|
|
1936
|
+
if (!allowed.has(key))
|
|
1937
|
+
throw new Error(`Unknown schedule override field: ${key}`);
|
|
1938
|
+
}
|
|
1939
|
+
if (hasOwn(overrides, "cron")) {
|
|
1940
|
+
next.cronOverride = normalizeScheduleCronOverride(overrides.cron);
|
|
1941
|
+
changedFields.add("cron");
|
|
1942
|
+
}
|
|
1943
|
+
if (hasOwn(overrides, "timezone")) {
|
|
1944
|
+
next.timezoneOverride = normalizeScheduleTimezoneOverride(overrides.timezone);
|
|
1945
|
+
changedFields.add("timezone");
|
|
1946
|
+
}
|
|
1947
|
+
if (hasOwn(overrides, "enabled")) {
|
|
1948
|
+
if (typeof overrides.enabled !== "boolean")
|
|
1949
|
+
throw new Error("Schedule enabled override must be boolean");
|
|
1950
|
+
next.enabledOverride = overrides.enabled ? 1 : 0;
|
|
1951
|
+
changedFields.add("enabled");
|
|
1952
|
+
}
|
|
1953
|
+
if (hasOwn(overrides, "input")) {
|
|
1954
|
+
next.inputJsonOverride = serializeScheduleJson(overrides.input);
|
|
1955
|
+
next.inputJsonOverrideEnabled = 1;
|
|
1956
|
+
changedFields.add("input");
|
|
1957
|
+
}
|
|
1958
|
+
if (hasOwn(overrides, "userMessage")) {
|
|
1959
|
+
next.userMessageOverride = normalizeScheduleUserMessage(overrides.userMessage, "Schedule user message override");
|
|
1960
|
+
changedFields.add("userMessage");
|
|
1961
|
+
}
|
|
1962
|
+
const now = input.now ?? nowIso();
|
|
1963
|
+
const afterEffective = effectiveAgentSchedule(next);
|
|
1964
|
+
next.nextRunAt = nextRunAfterScheduleChange({
|
|
1965
|
+
before: beforeEffective,
|
|
1966
|
+
after: afterEffective,
|
|
1967
|
+
currentNextRunAt: current.nextRunAt,
|
|
1968
|
+
now
|
|
1969
|
+
});
|
|
1970
|
+
if (stableJson(beforeEffective) !== stableJson(afterEffective) || next.nextRunAt !== current.nextRunAt) {
|
|
1971
|
+
next.dispatchGeneration = Number(current.dispatchGeneration ?? 1) + 1;
|
|
1972
|
+
next.dispatchAttempts = 0;
|
|
1973
|
+
next.dispatchLeaseExpiresAt = null;
|
|
1974
|
+
next.lastDispatchAt = null;
|
|
1975
|
+
next.lastDispatchError = null;
|
|
1976
|
+
}
|
|
1977
|
+
next.updatedAt = now;
|
|
1978
|
+
const row = await persistScheduleOverrideRow(control, next);
|
|
1979
|
+
return {
|
|
1980
|
+
row,
|
|
1981
|
+
before,
|
|
1982
|
+
after: agentScheduleAdminSnapshot(row),
|
|
1983
|
+
changedFields: [...changedFields]
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
export async function resetAgentScheduleOverrides(control, input) {
|
|
1987
|
+
const current = await loadAgentSchedule(control, input.id);
|
|
1988
|
+
const beforeEffective = effectiveAgentSchedule(current);
|
|
1989
|
+
const before = agentScheduleAdminSnapshot(current);
|
|
1990
|
+
const next = { ...current };
|
|
1991
|
+
const fields = input.fields?.length ? input.fields : ["cron", "timezone", "enabled", "input", "userMessage"];
|
|
1992
|
+
for (const field of fields)
|
|
1993
|
+
applyReset(next, field);
|
|
1994
|
+
const now = input.now ?? nowIso();
|
|
1995
|
+
const afterEffective = effectiveAgentSchedule(next);
|
|
1996
|
+
next.nextRunAt = nextRunAfterScheduleChange({
|
|
1997
|
+
before: beforeEffective,
|
|
1998
|
+
after: afterEffective,
|
|
1999
|
+
currentNextRunAt: current.nextRunAt,
|
|
2000
|
+
now
|
|
2001
|
+
});
|
|
2002
|
+
if (stableJson(beforeEffective) !== stableJson(afterEffective) || next.nextRunAt !== current.nextRunAt) {
|
|
2003
|
+
next.dispatchGeneration = Number(current.dispatchGeneration ?? 1) + 1;
|
|
2004
|
+
next.dispatchAttempts = 0;
|
|
2005
|
+
next.dispatchLeaseExpiresAt = null;
|
|
2006
|
+
next.lastDispatchAt = null;
|
|
2007
|
+
next.lastDispatchError = null;
|
|
2008
|
+
}
|
|
2009
|
+
next.updatedAt = now;
|
|
2010
|
+
const row = await persistScheduleOverrideRow(control, next);
|
|
2011
|
+
return {
|
|
2012
|
+
row,
|
|
2013
|
+
before,
|
|
2014
|
+
after: agentScheduleAdminSnapshot(row),
|
|
2015
|
+
changedFields: fields
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
export async function upsertAgentSchedule(control, input) {
|
|
2019
|
+
const now = input.now ?? nowIso();
|
|
2020
|
+
const idValue = agentScheduleId(input.agentName, input.triggerIndex, input.trigger);
|
|
2021
|
+
const existing = await findAgentSchedule(control, idValue);
|
|
2022
|
+
const row = {
|
|
2023
|
+
id: idValue,
|
|
2024
|
+
agentName: input.agentName,
|
|
2025
|
+
triggerIndex: input.triggerIndex,
|
|
2026
|
+
triggerKey: agentTriggerKey(input.agentName, input.triggerIndex, input.trigger),
|
|
2027
|
+
inputJson: stableJson(input.trigger.input ?? {}),
|
|
2028
|
+
cron: input.trigger.cron,
|
|
2029
|
+
timezone: input.trigger.timezone ?? null,
|
|
2030
|
+
enabled: input.trigger.enabled === false ? 0 : 1,
|
|
2031
|
+
userMessage: normalizeScheduleUserMessage(input.trigger.userMessage, "Schedule userMessage"),
|
|
2032
|
+
cronOverride: existing?.cronOverride ?? null,
|
|
2033
|
+
timezoneOverride: existing?.timezoneOverride ?? null,
|
|
2034
|
+
enabledOverride: normalizeOverrideEnabled(existing?.enabledOverride),
|
|
2035
|
+
inputJsonOverride: existing?.inputJsonOverride ?? null,
|
|
2036
|
+
inputJsonOverrideEnabled: existing?.inputJsonOverrideEnabled === 1 ? 1 : null,
|
|
2037
|
+
userMessageOverride: existing?.userMessageOverride ?? null,
|
|
2038
|
+
lastRunAt: existing?.lastRunAt ?? null,
|
|
2039
|
+
nextRunAt: null,
|
|
2040
|
+
service: input.service ?? "scheduler",
|
|
2041
|
+
dispatchGeneration: existing?.dispatchGeneration ?? 1,
|
|
2042
|
+
dispatchAttempts: existing?.dispatchAttempts ?? 0,
|
|
2043
|
+
dispatchLeaseExpiresAt: existing?.dispatchLeaseExpiresAt ?? null,
|
|
2044
|
+
lastDispatchAt: existing?.lastDispatchAt ?? null,
|
|
2045
|
+
lastDispatchError: existing?.lastDispatchError ?? null,
|
|
2046
|
+
createdAt: existing?.createdAt ?? now,
|
|
2047
|
+
updatedAt: now
|
|
2048
|
+
};
|
|
2049
|
+
row.nextRunAt = nextRunAfterScheduleChange({
|
|
2050
|
+
before: existing ? effectiveAgentSchedule(existing) : null,
|
|
2051
|
+
after: effectiveAgentSchedule(row),
|
|
2052
|
+
currentNextRunAt: existing?.nextRunAt ?? null,
|
|
2053
|
+
fallbackNextRunAt: input.nextRunAt,
|
|
2054
|
+
now
|
|
2055
|
+
});
|
|
2056
|
+
const definitionChanged = Boolean(existing) && (existing.agentName !== row.agentName
|
|
2057
|
+
|| existing.triggerIndex !== row.triggerIndex
|
|
2058
|
+
|| existing.triggerKey !== row.triggerKey
|
|
2059
|
+
|| existing.inputJson !== row.inputJson
|
|
2060
|
+
|| existing.cron !== row.cron
|
|
2061
|
+
|| existing.timezone !== row.timezone
|
|
2062
|
+
|| existing.enabled !== row.enabled
|
|
2063
|
+
|| existing.userMessage !== row.userMessage
|
|
2064
|
+
|| existing.nextRunAt !== row.nextRunAt);
|
|
2065
|
+
if (definitionChanged) {
|
|
2066
|
+
row.dispatchGeneration = Number(existing?.dispatchGeneration ?? 1) + 1;
|
|
2067
|
+
row.dispatchAttempts = 0;
|
|
2068
|
+
row.dispatchLeaseExpiresAt = null;
|
|
2069
|
+
row.lastDispatchAt = null;
|
|
2070
|
+
row.lastDispatchError = null;
|
|
2071
|
+
}
|
|
2072
|
+
await control.run(`INSERT INTO ${q("_agent_schedules")}
|
|
2073
|
+
("id", "agentName", "triggerIndex", "triggerKey", "inputJson", "cron", "timezone", "enabled", "userMessage",
|
|
2074
|
+
"cronOverride", "timezoneOverride", "enabledOverride", "inputJsonOverride", "inputJsonOverrideEnabled", "userMessageOverride",
|
|
2075
|
+
"lastRunAt", "nextRunAt", "service", "dispatchGeneration", "dispatchAttempts", "dispatchLeaseExpiresAt",
|
|
2076
|
+
"lastDispatchAt", "lastDispatchError", "createdAt", "updatedAt")
|
|
2077
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2078
|
+
ON CONFLICT("id") DO UPDATE SET
|
|
2079
|
+
"agentName" = excluded."agentName",
|
|
2080
|
+
"triggerIndex" = excluded."triggerIndex",
|
|
2081
|
+
"triggerKey" = excluded."triggerKey",
|
|
2082
|
+
"inputJson" = excluded."inputJson",
|
|
2083
|
+
"cron" = excluded."cron",
|
|
2084
|
+
"timezone" = excluded."timezone",
|
|
2085
|
+
"enabled" = excluded."enabled",
|
|
2086
|
+
"userMessage" = excluded."userMessage",
|
|
2087
|
+
"lastRunAt" = excluded."lastRunAt",
|
|
2088
|
+
"nextRunAt" = excluded."nextRunAt",
|
|
2089
|
+
"service" = excluded."service",
|
|
2090
|
+
"dispatchGeneration" = excluded."dispatchGeneration",
|
|
2091
|
+
"dispatchAttempts" = excluded."dispatchAttempts",
|
|
2092
|
+
"dispatchLeaseExpiresAt" = excluded."dispatchLeaseExpiresAt",
|
|
2093
|
+
"lastDispatchAt" = excluded."lastDispatchAt",
|
|
2094
|
+
"lastDispatchError" = excluded."lastDispatchError",
|
|
2095
|
+
"updatedAt" = excluded."updatedAt"`, [
|
|
2096
|
+
row.id,
|
|
2097
|
+
row.agentName,
|
|
2098
|
+
row.triggerIndex,
|
|
2099
|
+
row.triggerKey,
|
|
2100
|
+
row.inputJson,
|
|
2101
|
+
row.cron,
|
|
2102
|
+
row.timezone,
|
|
2103
|
+
row.enabled,
|
|
2104
|
+
row.userMessage,
|
|
2105
|
+
row.cronOverride,
|
|
2106
|
+
row.timezoneOverride,
|
|
2107
|
+
row.enabledOverride,
|
|
2108
|
+
row.inputJsonOverride,
|
|
2109
|
+
row.inputJsonOverrideEnabled,
|
|
2110
|
+
row.userMessageOverride,
|
|
2111
|
+
row.lastRunAt,
|
|
2112
|
+
row.nextRunAt,
|
|
2113
|
+
row.service,
|
|
2114
|
+
row.dispatchGeneration,
|
|
2115
|
+
row.dispatchAttempts,
|
|
2116
|
+
row.dispatchLeaseExpiresAt,
|
|
2117
|
+
row.lastDispatchAt,
|
|
2118
|
+
row.lastDispatchError,
|
|
2119
|
+
row.createdAt,
|
|
2120
|
+
row.updatedAt
|
|
2121
|
+
]);
|
|
2122
|
+
return (await control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ?`, [row.id])) ?? row;
|
|
2123
|
+
}
|
|
2124
|
+
export async function listDueAgentSchedules(control, input) {
|
|
2125
|
+
const limit = Math.max(1, Math.min(500, input.limit ?? 100));
|
|
2126
|
+
return control.all(`SELECT * FROM ${q("_agent_schedules")}
|
|
2127
|
+
WHERE (("enabledOverride" IS NULL AND "enabled" = 1) OR "enabledOverride" = 1)
|
|
2128
|
+
AND "nextRunAt" IS NOT NULL AND "nextRunAt" <= ?
|
|
2129
|
+
ORDER BY "nextRunAt" ASC
|
|
2130
|
+
LIMIT ?`, [input.now, limit]);
|
|
2131
|
+
}
|
|
2132
|
+
export const agentScheduleMaxDispatchAttempts = 5;
|
|
2133
|
+
export async function claimDueAgentSchedules(control, input) {
|
|
2134
|
+
const limit = Math.max(1, Math.min(500, input.limit ?? 100));
|
|
2135
|
+
const maxAttempts = Math.max(1, input.maxAttempts ?? agentScheduleMaxDispatchAttempts);
|
|
2136
|
+
await control.run(`UPDATE ${q("_agent_schedules")} SET "enabled" = 0, "enabledOverride" = 0,
|
|
2137
|
+
"lastDispatchError" = 'dispatch-attempts-exhausted', "dispatchLeaseExpiresAt" = NULL, "updatedAt" = ?
|
|
2138
|
+
WHERE (("enabledOverride" IS NULL AND "enabled" = 1) OR "enabledOverride" = 1)
|
|
2139
|
+
AND "nextRunAt" IS NOT NULL AND "nextRunAt" <= ? AND COALESCE("dispatchAttempts", 0) >= ?
|
|
2140
|
+
AND ("dispatchLeaseExpiresAt" IS NULL OR "dispatchLeaseExpiresAt" <= ?)`, [input.now, input.now, maxAttempts, input.wallNow]);
|
|
2141
|
+
const due = await control.all(`SELECT * FROM ${q("_agent_schedules")}
|
|
2142
|
+
WHERE (("enabledOverride" IS NULL AND "enabled" = 1) OR "enabledOverride" = 1)
|
|
2143
|
+
AND "nextRunAt" IS NOT NULL AND "nextRunAt" <= ? AND COALESCE("dispatchAttempts", 0) < ?
|
|
2144
|
+
AND ("dispatchLeaseExpiresAt" IS NULL OR "dispatchLeaseExpiresAt" <= ?)
|
|
2145
|
+
ORDER BY "nextRunAt" ASC LIMIT ?`, [input.now, maxAttempts, input.wallNow, limit]);
|
|
2146
|
+
const claimed = [];
|
|
2147
|
+
for (const row of due) {
|
|
2148
|
+
if (!row.nextRunAt)
|
|
2149
|
+
continue;
|
|
2150
|
+
const generation = Number(row.dispatchGeneration ?? 1);
|
|
2151
|
+
const attempts = Number(row.dispatchAttempts ?? 0);
|
|
2152
|
+
await control.run(`UPDATE ${q("_agent_schedules")} SET "dispatchGeneration" = ?, "dispatchAttempts" = ?,
|
|
2153
|
+
"dispatchLeaseExpiresAt" = ?, "lastDispatchAt" = ?, "lastDispatchError" = NULL, "updatedAt" = ?
|
|
2154
|
+
WHERE "id" = ? AND COALESCE("dispatchGeneration", 1) = ? AND COALESCE("dispatchAttempts", 0) = ?
|
|
2155
|
+
AND "nextRunAt" = ? AND ("dispatchLeaseExpiresAt" IS NULL OR "dispatchLeaseExpiresAt" <= ?)`, [generation, attempts + 1, input.leaseExpiresAt, input.wallNow, input.now, row.id, generation, attempts, row.nextRunAt, input.wallNow]);
|
|
2156
|
+
const current = await control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ? LIMIT 1`, [row.id]);
|
|
2157
|
+
if (current?.nextRunAt === row.nextRunAt
|
|
2158
|
+
&& Number(current.dispatchGeneration ?? 1) === generation
|
|
2159
|
+
&& Number(current.dispatchAttempts ?? 0) === attempts + 1
|
|
2160
|
+
&& current.dispatchLeaseExpiresAt === input.leaseExpiresAt) {
|
|
2161
|
+
claimed.push({ id: current.id, agentName: current.agentName, generation, bookedAt: current.nextRunAt });
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return claimed;
|
|
2165
|
+
}
|
|
2166
|
+
export async function loadClaimedAgentSchedule(control, input) {
|
|
2167
|
+
return control.first(`SELECT * FROM ${q("_agent_schedules")} WHERE "id" = ? AND "nextRunAt" = ?
|
|
2168
|
+
AND COALESCE("dispatchGeneration", 1) = ? LIMIT 1`, [input.id, input.bookedAt, input.generation]);
|
|
2169
|
+
}
|
|
2170
|
+
export async function releaseAgentScheduleDispatch(control, input) {
|
|
2171
|
+
await control.run(`UPDATE ${q("_agent_schedules")} SET "dispatchLeaseExpiresAt" = NULL, "lastDispatchError" = ?, "updatedAt" = ?
|
|
2172
|
+
WHERE "id" = ? AND "nextRunAt" = ? AND COALESCE("dispatchGeneration", 1) = ?`, [input.error, input.now ?? nowIso(), input.id, input.bookedAt, input.generation]);
|
|
2173
|
+
}
|
|
2174
|
+
export async function advanceAgentSchedule(control, input) {
|
|
2175
|
+
await control.run(`UPDATE ${q("_agent_schedules")} SET "lastRunAt" = ?, "nextRunAt" = ?,
|
|
2176
|
+
"dispatchGeneration" = COALESCE("dispatchGeneration", 1) + 1, "dispatchAttempts" = 0,
|
|
2177
|
+
"dispatchLeaseExpiresAt" = NULL, "lastDispatchAt" = NULL, "lastDispatchError" = NULL, "updatedAt" = ?
|
|
2178
|
+
WHERE "id" = ? ${input.generation === undefined ? "" : `AND COALESCE("dispatchGeneration", 1) = ? AND "nextRunAt" = ?`}`, [input.lastRunAt, input.nextRunAt, input.now ?? nowIso(), input.id,
|
|
2179
|
+
...(input.generation === undefined ? [] : [input.generation, input.lastRunAt])]);
|
|
2180
|
+
}
|
|
2181
|
+
export async function createAgentTriggerEvent(control, input) {
|
|
2182
|
+
const now = input.now ?? wallNowIso();
|
|
2183
|
+
const row = {
|
|
2184
|
+
id: randomId("agent_evt"),
|
|
2185
|
+
agentName: input.agentName,
|
|
2186
|
+
triggerType: input.triggerType,
|
|
2187
|
+
triggerKey: input.triggerKey,
|
|
2188
|
+
sourceTable: input.sourceTable ?? null,
|
|
2189
|
+
sourceOp: input.sourceOp ?? null,
|
|
2190
|
+
rowId: input.rowId ?? null,
|
|
2191
|
+
inputJson: input.input === undefined ? null : stableJson(input.input),
|
|
2192
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
2193
|
+
status: input.status ?? "queued",
|
|
2194
|
+
runId: null,
|
|
2195
|
+
error: input.error ?? null,
|
|
2196
|
+
createdAt: now,
|
|
2197
|
+
updatedAt: now
|
|
2198
|
+
};
|
|
2199
|
+
await control.run(`INSERT INTO ${q("_agent_trigger_events")}
|
|
2200
|
+
("id", "agentName", "triggerType", "triggerKey", "sourceTable", "sourceOp", "rowId", "inputJson", "idempotencyKey", "status", "runId", "error", "createdAt", "updatedAt")
|
|
2201
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2202
|
+
row.id,
|
|
2203
|
+
row.agentName,
|
|
2204
|
+
row.triggerType,
|
|
2205
|
+
row.triggerKey,
|
|
2206
|
+
row.sourceTable,
|
|
2207
|
+
row.sourceOp,
|
|
2208
|
+
row.rowId,
|
|
2209
|
+
row.inputJson,
|
|
2210
|
+
row.idempotencyKey,
|
|
2211
|
+
row.status,
|
|
2212
|
+
row.runId,
|
|
2213
|
+
row.error,
|
|
2214
|
+
row.createdAt,
|
|
2215
|
+
row.updatedAt
|
|
2216
|
+
]);
|
|
2217
|
+
return row;
|
|
2218
|
+
}
|
|
2219
|
+
async function markAgentTriggerEvent(control, eventId, input) {
|
|
2220
|
+
await control.run(`UPDATE ${q("_agent_trigger_events")} SET "status" = ?, "runId" = COALESCE(?, "runId"), "error" = ?, "updatedAt" = ? WHERE "id" = ?`, [input.status, input.runId ?? null, input.error ?? null, input.now ?? wallNowIso(), eventId]);
|
|
2221
|
+
}
|
|
2222
|
+
export const markAgentTriggerEventQueued = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "queued" });
|
|
2223
|
+
export const markAgentTriggerEventRunning = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "running" });
|
|
2224
|
+
export const markAgentTriggerEventCompleted = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "completed" });
|
|
2225
|
+
export const markAgentTriggerEventFailed = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "failed" });
|
|
2226
|
+
export const markAgentTriggerEventSkipped = (control, eventId, input = {}) => markAgentTriggerEvent(control, eventId, { ...input, status: "skipped" });
|
|
2227
|
+
export async function loadAgentTriggerEvent(control, eventId) {
|
|
2228
|
+
const row = await control.first(`SELECT * FROM ${q("_agent_trigger_events")} WHERE "id" = ? LIMIT 1`, [eventId]);
|
|
2229
|
+
if (!row)
|
|
2230
|
+
throw new AgentHttpError("Agent trigger event not found", 404);
|
|
2231
|
+
return row;
|
|
2232
|
+
}
|
|
2233
|
+
export async function findAgentTriggerEventByIdempotencyKey(control, idempotencyKey) {
|
|
2234
|
+
return control.first(`SELECT * FROM ${q("_agent_trigger_events")} WHERE "idempotencyKey" = ? LIMIT 1`, [idempotencyKey]);
|
|
2235
|
+
}
|
|
2236
|
+
export function directAgentTriggerKey(agentName) {
|
|
2237
|
+
return `direct:${agentName}`;
|
|
2238
|
+
}
|
|
2239
|
+
function directBindingFingerprintFromInput(inputJson) {
|
|
2240
|
+
if (!inputJson)
|
|
2241
|
+
return "new";
|
|
2242
|
+
try {
|
|
2243
|
+
const input = JSON.parse(inputJson);
|
|
2244
|
+
const conversation = input.conversation;
|
|
2245
|
+
if (!conversation || typeof conversation !== "object")
|
|
2246
|
+
return "new";
|
|
2247
|
+
const record = conversation;
|
|
2248
|
+
// Compare the derived id, not the raw external thread key, so replay checks
|
|
2249
|
+
// do not depend on provider ids being used as storage identifiers.
|
|
2250
|
+
if (record.mode === "thread" && typeof record.conversationId === "string") {
|
|
2251
|
+
return `thread:${record.conversationId}`;
|
|
2252
|
+
}
|
|
2253
|
+
if (record.mode === "chat" && typeof record.conversationId === "string") {
|
|
2254
|
+
return `chat:${record.conversationId}`;
|
|
2255
|
+
}
|
|
2256
|
+
return "new";
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
return "new";
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
export async function findAgentRun(control, runId) {
|
|
2263
|
+
return control.first(`SELECT * FROM ${q("_agent_runs")} WHERE "runId" = ? LIMIT 1`, [runId]);
|
|
2264
|
+
}
|
|
2265
|
+
export async function loadAgentRun(control, runId) {
|
|
2266
|
+
const row = await findAgentRun(control, runId);
|
|
2267
|
+
if (!row)
|
|
2268
|
+
throw new AgentHttpError("Agent run not found", 404);
|
|
2269
|
+
return row;
|
|
2270
|
+
}
|
|
2271
|
+
export async function createOrLoadDirectAgentInvocationRecord(input) {
|
|
2272
|
+
const triggerKey = directAgentTriggerKey(input.agent.name);
|
|
2273
|
+
if (input.idempotencyKey !== undefined && input.idempotencyKey !== null) {
|
|
2274
|
+
const existing = await findAgentTriggerEventByIdempotencyKey(input.control, input.idempotencyKey);
|
|
2275
|
+
if (existing) {
|
|
2276
|
+
if (existing.agentName !== input.agent.name || existing.triggerType !== "direct" || existing.triggerKey !== triggerKey) {
|
|
2277
|
+
throw new Error(`Agent idempotency key conflict for ${input.agent.name}`);
|
|
2278
|
+
}
|
|
2279
|
+
const existingFingerprint = directBindingFingerprintFromInput(existing.inputJson);
|
|
2280
|
+
if ((input.bindingFingerprint ?? "new") !== existingFingerprint) {
|
|
2281
|
+
throw new Error(`Agent idempotency key conflict for ${input.agent.name}: conversation binding changed`);
|
|
2282
|
+
}
|
|
2283
|
+
return {
|
|
2284
|
+
created: false,
|
|
2285
|
+
event: existing,
|
|
2286
|
+
run: existing.runId ? await findAgentRun(input.control, existing.runId) : null
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const event = await createAgentTriggerEvent(input.control, {
|
|
2291
|
+
agentName: input.agent.name,
|
|
2292
|
+
triggerType: "direct",
|
|
2293
|
+
triggerKey,
|
|
2294
|
+
input: input.invocationInput,
|
|
2295
|
+
idempotencyKey: input.idempotencyKey ?? null
|
|
2296
|
+
});
|
|
2297
|
+
const run = await createAgentAutomationRun({
|
|
2298
|
+
control: input.control,
|
|
2299
|
+
agent: input.agent,
|
|
2300
|
+
triggerType: "direct",
|
|
2301
|
+
eventId: event.id,
|
|
2302
|
+
requestId: input.requestId,
|
|
2303
|
+
metadata: input.metadata,
|
|
2304
|
+
conversationId: input.conversationId
|
|
2305
|
+
});
|
|
2306
|
+
await markAgentTriggerEventQueued(input.control, event.id, { runId: run.runId });
|
|
2307
|
+
return {
|
|
2308
|
+
created: true,
|
|
2309
|
+
event: { ...event, runId: run.runId },
|
|
2310
|
+
run
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
export async function createAgentAutomationRun(input) {
|
|
2314
|
+
const now = nowIso();
|
|
2315
|
+
const run = {
|
|
2316
|
+
runId: randomId("agent_run"),
|
|
2317
|
+
conversationId: input.conversationId ?? (input.triggerType === "direct" ? `agent_direct:${input.eventId}` : `agent_auto:${input.eventId}`),
|
|
2318
|
+
agentName: input.agent.name,
|
|
2319
|
+
requestId: input.requestId ?? randomId("agent_req"),
|
|
2320
|
+
triggerType: input.triggerType,
|
|
2321
|
+
inputMessageId: null,
|
|
2322
|
+
status: "queued",
|
|
2323
|
+
startedAt: now,
|
|
2324
|
+
heartbeatAt: now,
|
|
2325
|
+
finishedAt: null,
|
|
2326
|
+
error: null,
|
|
2327
|
+
usageEventId: null,
|
|
2328
|
+
auditEventId: null,
|
|
2329
|
+
metadataJson: input.metadata === undefined ? null : stableJson(input.metadata)
|
|
2330
|
+
};
|
|
2331
|
+
await input.control.run(`INSERT INTO ${q("_agent_runs")}
|
|
2332
|
+
("runId", "conversationId", "agentName", "requestId", "triggerType", "inputMessageId", "status", "startedAt", "heartbeatAt", "finishedAt",
|
|
2333
|
+
"error", "usageEventId", "auditEventId", "metadataJson")
|
|
2334
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2335
|
+
run.runId,
|
|
2336
|
+
run.conversationId,
|
|
2337
|
+
run.agentName,
|
|
2338
|
+
run.requestId,
|
|
2339
|
+
run.triggerType,
|
|
2340
|
+
run.inputMessageId,
|
|
2341
|
+
run.status,
|
|
2342
|
+
run.startedAt,
|
|
2343
|
+
run.heartbeatAt,
|
|
2344
|
+
run.finishedAt,
|
|
2345
|
+
run.error,
|
|
2346
|
+
run.usageEventId,
|
|
2347
|
+
run.auditEventId,
|
|
2348
|
+
run.metadataJson
|
|
2349
|
+
]);
|
|
2350
|
+
return run;
|
|
2351
|
+
}
|
|
2352
|
+
// A crashed isolate can leave a running run with no writer left to finish it;
|
|
2353
|
+
// past this age it no longer blocks a retry. Queue wait is deliberately
|
|
2354
|
+
// governed by a separate, much longer horizon below: work that has not
|
|
2355
|
+
// started must not spend its execution budget while waiting for capacity.
|
|
2356
|
+
export const activeAgentRunStaleSeconds = 5 * 60;
|
|
2357
|
+
export const queuedAgentRunStaleSeconds = 24 * 60 * 60;
|
|
2358
|
+
export async function findActiveAgentRun(control, input) {
|
|
2359
|
+
const now = input.now ?? nowIso();
|
|
2360
|
+
const nowMs = new Date(now).getTime();
|
|
2361
|
+
const runningStaleBefore = new Date(nowMs - activeAgentRunStaleSeconds * 1000).toISOString();
|
|
2362
|
+
const queuedStaleBefore = new Date(nowMs - queuedAgentRunStaleSeconds * 1000).toISOString();
|
|
2363
|
+
return control.first(`SELECT * FROM ${q("_agent_runs")}
|
|
2364
|
+
WHERE "agentName" = ? AND "conversationId" = ?
|
|
2365
|
+
AND (("status" = 'running' AND COALESCE("heartbeatAt", "startedAt") > ?) OR ("status" = 'queued' AND "startedAt" > ?))
|
|
2366
|
+
ORDER BY "startedAt" DESC LIMIT 1`, [input.agentName, input.conversationId, runningStaleBefore, queuedStaleBefore]);
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* A run whose writer is known to be gone rather than merely late.
|
|
2370
|
+
*
|
|
2371
|
+
* The staleness sweep does not write this and must not: from a horizon it
|
|
2372
|
+
* cannot tell a restart from a turn that simply ran long, and claiming a
|
|
2373
|
+
* restart sends debugging after reloads that never happened. The conversation
|
|
2374
|
+
* object can tell, because it knows which runs the isolate it is running in
|
|
2375
|
+
* actually holds, so it settles its own orphans under this label the moment it
|
|
2376
|
+
* next reconciles (`isOrphanedRun` in agent-ai-chat.ts). Rows written by
|
|
2377
|
+
* earlier builds carry it too, which is why isReapedAgentRunError still
|
|
2378
|
+
* matches it.
|
|
2379
|
+
*/
|
|
2380
|
+
export const interruptedAgentRunError = "Interrupted by restart or deployment";
|
|
2381
|
+
const staleAgentRunErrorPrefix = "Timed out: no completion within";
|
|
2382
|
+
const staleQueuedAgentRunErrorPrefix = "Timed out: did not start within";
|
|
2383
|
+
/**
|
|
2384
|
+
* What the execution sweep actually knows: the run was still running when it
|
|
2385
|
+
* passed the horizon. Names the configured horizon so a reader can tell
|
|
2386
|
+
* "this turn was too slow" from "something killed the isolate".
|
|
2387
|
+
*/
|
|
2388
|
+
export function staleAgentRunError(staleSeconds = activeAgentRunStaleSeconds) {
|
|
2389
|
+
return `${staleAgentRunErrorPrefix} the ${staleSeconds}s agent run staleness horizon`;
|
|
2390
|
+
}
|
|
2391
|
+
/** Truthful terminal label for work that remained queued without starting. */
|
|
2392
|
+
export function staleQueuedAgentRunError(staleSeconds = queuedAgentRunStaleSeconds) {
|
|
2393
|
+
return `${staleQueuedAgentRunErrorPrefix} the ${staleSeconds}s agent queue-wait horizon`;
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* True for any run the staleness sweep marked failed, under either current
|
|
2397
|
+
* label or the legacy one. Retry, redispatch and coverage checks
|
|
2398
|
+
* treat such a run as "no live writer" rather than as a real turn outcome, so
|
|
2399
|
+
* they must match both.
|
|
2400
|
+
*/
|
|
2401
|
+
export function isReapedAgentRunError(error) {
|
|
2402
|
+
if (!error)
|
|
2403
|
+
return false;
|
|
2404
|
+
return error === interruptedAgentRunError
|
|
2405
|
+
|| error.startsWith(staleAgentRunErrorPrefix)
|
|
2406
|
+
|| error.startsWith(staleQueuedAgentRunErrorPrefix);
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Terminal bookkeeping for runs no writer is left to finish. A deploy or
|
|
2410
|
+
* crash kills the writer between run creation and the completed/failed
|
|
2411
|
+
* update, so the row sits at running with finishedAt null forever and runs
|
|
2412
|
+
* evidence reports a turn that is not executing.
|
|
2413
|
+
*
|
|
2414
|
+
* A running row is judged on `heartbeatAt`, not on when it started (2026-08-25).
|
|
2415
|
+
* On `startedAt` this swept up **live** turns: a turn is not late because it is
|
|
2416
|
+
* long, and a nine-round tool loop passes five minutes without anything being
|
|
2417
|
+
* wrong with it. What that cost was not a stray row — it was a member reading
|
|
2418
|
+
* "Birch ran into a problem while answering" twenty-four seconds before the
|
|
2419
|
+
* same run delivered its answer, because the sweep's terminal write is what the
|
|
2420
|
+
* conversation reports. The heartbeat is touched per model step, so a wedged
|
|
2421
|
+
* turn stops beating and is still reaped on time, which is the whole point of
|
|
2422
|
+
* having a horizon. Rows written before the column coalesce to `startedAt` and
|
|
2423
|
+
* are swept exactly as they were.
|
|
2424
|
+
*
|
|
2425
|
+
* A queued row keeps its own queue-wait horizon against `startedAt` and its own
|
|
2426
|
+
* label, because its execution has not begun and there is nothing to beat.
|
|
2427
|
+
* Status and timestamp predicates are re-checked in each UPDATE so adoption
|
|
2428
|
+
* cannot be reaped using its former queue clock.
|
|
2429
|
+
*/
|
|
2430
|
+
export async function sweepStaleAgentRuns(control, input = {}) {
|
|
2431
|
+
const now = input.now ?? nowIso();
|
|
2432
|
+
const nowMs = new Date(now).getTime();
|
|
2433
|
+
const runningStaleBefore = new Date(nowMs - activeAgentRunStaleSeconds * 1000).toISOString();
|
|
2434
|
+
const queuedStaleBefore = new Date(nowMs - queuedAgentRunStaleSeconds * 1000).toISOString();
|
|
2435
|
+
const limit = Math.max(1, Math.min(500, input.limit ?? 100));
|
|
2436
|
+
const stale = await control.all(`SELECT "runId", "status" FROM ${q("_agent_runs")}
|
|
2437
|
+
WHERE ("status" = 'running' AND COALESCE("heartbeatAt", "startedAt") <= ?)
|
|
2438
|
+
OR ("status" = 'queued' AND "startedAt" <= ?)
|
|
2439
|
+
ORDER BY "startedAt" ASC LIMIT ?`, [runningStaleBefore, queuedStaleBefore, limit]);
|
|
2440
|
+
if (stale.length === 0)
|
|
2441
|
+
return [];
|
|
2442
|
+
const runningIds = stale.filter((row) => row.status === "running").map((row) => row.runId);
|
|
2443
|
+
const queuedIds = stale.filter((row) => row.status === "queued").map((row) => row.runId);
|
|
2444
|
+
if (runningIds.length) {
|
|
2445
|
+
await control.run(`UPDATE ${q("_agent_runs")}
|
|
2446
|
+
SET "status" = 'failed', "error" = ?, "finishedAt" = ?
|
|
2447
|
+
WHERE "runId" IN (${runningIds.map(() => "?").join(", ")})
|
|
2448
|
+
AND "status" = 'running' AND COALESCE("heartbeatAt", "startedAt") <= ?`, [staleAgentRunError(), now, ...runningIds, runningStaleBefore]);
|
|
2449
|
+
}
|
|
2450
|
+
if (queuedIds.length) {
|
|
2451
|
+
await control.run(`UPDATE ${q("_agent_runs")}
|
|
2452
|
+
SET "status" = 'failed', "error" = ?, "finishedAt" = ?
|
|
2453
|
+
WHERE "runId" IN (${queuedIds.map(() => "?").join(", ")})
|
|
2454
|
+
AND "status" = 'queued' AND "startedAt" <= ?`, [staleQueuedAgentRunError(), now, ...queuedIds, queuedStaleBefore]);
|
|
2455
|
+
}
|
|
2456
|
+
return stale.map((row) => row.runId);
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Deletes poll/idempotency evidence after each owning Agent's configured
|
|
2460
|
+
* evidence window. Request replay and outcome polling are guaranteed only
|
|
2461
|
+
* while their receipt and run evidence remain inside that window.
|
|
2462
|
+
*/
|
|
2463
|
+
export async function sweepAgentRunEvidence(control, input = {}) {
|
|
2464
|
+
const nowMs = new Date(input.now ?? nowIso()).getTime();
|
|
2465
|
+
const limit = Math.max(1, Math.min(2_000, input.limit ?? 500));
|
|
2466
|
+
const ttlForAgent = input.runEvidenceTtlMsForAgent
|
|
2467
|
+
?? (() => defaultAgentRunEvidenceTtlMs);
|
|
2468
|
+
const pageSize = Math.min(500, Math.max(100, limit));
|
|
2469
|
+
const earliestPossibleExpiry = new Date(nowMs - minAgentRunEvidenceTtlMs).toISOString();
|
|
2470
|
+
const runIds = [];
|
|
2471
|
+
let runCursor = null;
|
|
2472
|
+
while (runIds.length < limit) {
|
|
2473
|
+
const terminalRuns = await control.all(`SELECT "runId", "agentName", "finishedAt"
|
|
2474
|
+
FROM ${q("_agent_runs")}
|
|
2475
|
+
WHERE "status" IN ('completed', 'cancelled', 'failed', 'skipped')
|
|
2476
|
+
AND "finishedAt" IS NOT NULL
|
|
2477
|
+
AND "finishedAt" < ?
|
|
2478
|
+
${runCursor ? `AND ("finishedAt" > ? OR ("finishedAt" = ? AND "runId" > ?))` : ""}
|
|
2479
|
+
ORDER BY "finishedAt" ASC, "runId" ASC
|
|
2480
|
+
LIMIT ?`, runCursor
|
|
2481
|
+
? [earliestPossibleExpiry, runCursor.finishedAt, runCursor.finishedAt, runCursor.runId, pageSize]
|
|
2482
|
+
: [earliestPossibleExpiry, pageSize]);
|
|
2483
|
+
for (const row of terminalRuns) {
|
|
2484
|
+
const finishedAtMs = row.finishedAt ? new Date(row.finishedAt).getTime() : Number.NaN;
|
|
2485
|
+
const ttlMs = ttlForAgent(row.agentName);
|
|
2486
|
+
if (Number.isFinite(finishedAtMs) && Number.isFinite(ttlMs) && nowMs - finishedAtMs > ttlMs) {
|
|
2487
|
+
runIds.push(row.runId);
|
|
2488
|
+
if (runIds.length === limit)
|
|
2489
|
+
break;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
const last = terminalRuns.at(-1);
|
|
2493
|
+
if (!last?.finishedAt || terminalRuns.length < pageSize || runIds.length === limit)
|
|
2494
|
+
break;
|
|
2495
|
+
runCursor = { finishedAt: last.finishedAt, runId: last.runId };
|
|
2496
|
+
}
|
|
2497
|
+
for (let offset = 0; offset < runIds.length; offset += 50) {
|
|
2498
|
+
const batch = runIds.slice(offset, offset + 50);
|
|
2499
|
+
await control.run(`DELETE FROM ${q("_agent_runs")} WHERE "runId" IN (${batch.map(() => "?").join(", ")})`, batch);
|
|
2500
|
+
}
|
|
2501
|
+
const requestIds = [];
|
|
2502
|
+
let receiptCursor = null;
|
|
2503
|
+
while (requestIds.length < limit) {
|
|
2504
|
+
const receipts = await control.all(`SELECT r."id", r."createdAt", i."agentName"
|
|
2505
|
+
FROM ${q("_agent_requests")} r
|
|
2506
|
+
LEFT JOIN ${q("_agent_instances")} i ON i."conversationId" = r."conversationId"
|
|
2507
|
+
WHERE r."createdAt" < ?
|
|
2508
|
+
${receiptCursor ? `AND (r."createdAt" > ? OR (r."createdAt" = ? AND r."id" > ?))` : ""}
|
|
2509
|
+
ORDER BY r."createdAt" ASC, r."id" ASC
|
|
2510
|
+
LIMIT ?`, receiptCursor
|
|
2511
|
+
? [earliestPossibleExpiry, receiptCursor.createdAt, receiptCursor.createdAt, receiptCursor.id, pageSize]
|
|
2512
|
+
: [earliestPossibleExpiry, pageSize]);
|
|
2513
|
+
for (const row of receipts) {
|
|
2514
|
+
const createdAtMs = new Date(row.createdAt).getTime();
|
|
2515
|
+
const ttlMs = row.agentName
|
|
2516
|
+
? ttlForAgent(row.agentName)
|
|
2517
|
+
: defaultAgentRunEvidenceTtlMs;
|
|
2518
|
+
if (Number.isFinite(createdAtMs) && Number.isFinite(ttlMs) && nowMs - createdAtMs > ttlMs) {
|
|
2519
|
+
requestIds.push(row.id);
|
|
2520
|
+
if (requestIds.length === limit)
|
|
2521
|
+
break;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const last = receipts.at(-1);
|
|
2525
|
+
if (!last || receipts.length < pageSize || requestIds.length === limit)
|
|
2526
|
+
break;
|
|
2527
|
+
receiptCursor = { createdAt: last.createdAt, id: last.id };
|
|
2528
|
+
}
|
|
2529
|
+
for (let offset = 0; offset < requestIds.length; offset += 50) {
|
|
2530
|
+
const batch = requestIds.slice(offset, offset + 50);
|
|
2531
|
+
await control.run(`DELETE FROM ${q("_agent_requests")} WHERE "id" IN (${batch.map(() => "?").join(", ")})`, batch);
|
|
2532
|
+
}
|
|
2533
|
+
return { runIds, requestIds };
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Mark a run as still working, without touching anything else about it.
|
|
2537
|
+
*
|
|
2538
|
+
* One column, one row, no read first — it runs once per model step inside a
|
|
2539
|
+
* turn that is already doing far more expensive things, and it must never be
|
|
2540
|
+
* the reason a turn fails. A run that has already reached a terminal status is
|
|
2541
|
+
* left alone by the status predicate rather than by a check here, so a beat
|
|
2542
|
+
* that lands in the teardown race cannot resurrect a finished row.
|
|
2543
|
+
*/
|
|
2544
|
+
export async function touchAgentRunHeartbeat(control, runId, now = nowIso()) {
|
|
2545
|
+
await control.run(`UPDATE ${q("_agent_runs")} SET "heartbeatAt" = ? WHERE "runId" = ? AND "status" = 'running'`, [now, runId]);
|
|
2546
|
+
}
|
|
2547
|
+
export async function updateAgentRun(control, runId, input) {
|
|
2548
|
+
const current = await control.first(`SELECT * FROM ${q("_agent_runs")} WHERE "runId" = ? LIMIT 1`, [runId]);
|
|
2549
|
+
if (!current)
|
|
2550
|
+
throw new AgentHttpError("Agent run not found", 404);
|
|
2551
|
+
const status = input.status ?? current.status;
|
|
2552
|
+
const startedAt = input.startedAt
|
|
2553
|
+
?? (current.status === "queued" && status === "running" ? nowIso() : current.startedAt);
|
|
2554
|
+
const finishedAt = input.finishedAt !== undefined
|
|
2555
|
+
? input.finishedAt
|
|
2556
|
+
: status === "completed" || status === "cancelled" || status === "failed" || status === "skipped"
|
|
2557
|
+
? nowIso()
|
|
2558
|
+
: current.finishedAt;
|
|
2559
|
+
// Execution starting is itself a sign of life, and the queue wait before it
|
|
2560
|
+
// is not: a row that sat queued for an hour must not begin its running life
|
|
2561
|
+
// already an hour stale. Every other update leaves the mark where the turn's
|
|
2562
|
+
// own steps put it.
|
|
2563
|
+
const heartbeatAt = startedAt !== current.startedAt ? startedAt : current.heartbeatAt;
|
|
2564
|
+
await control.run(`UPDATE ${q("_agent_runs")}
|
|
2565
|
+
SET "status" = ?, "startedAt" = ?, "heartbeatAt" = ?, "error" = ?, "finishedAt" = ?, "metadataJson" = ?
|
|
2566
|
+
WHERE "runId" = ?`, [
|
|
2567
|
+
status,
|
|
2568
|
+
startedAt,
|
|
2569
|
+
heartbeatAt,
|
|
2570
|
+
input.error !== undefined ? input.error : current.error,
|
|
2571
|
+
finishedAt,
|
|
2572
|
+
input.metadata !== undefined ? stableJson(input.metadata) : current.metadataJson,
|
|
2573
|
+
runId
|
|
2574
|
+
]);
|
|
2575
|
+
}
|
|
2576
|
+
export async function touchAgentInstance(control, instanceId, input = {}) {
|
|
2577
|
+
const now = nowIso();
|
|
2578
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2579
|
+
SET "lastActiveAt" = ?, "expiresAt" = ?, "updatedAt" = ?, "lastError" = ?
|
|
2580
|
+
WHERE "id" = ?`, [now, agentExpiresAt(input.ttlSeconds), now, input.lastError ?? null, instanceId]);
|
|
2581
|
+
}
|
|
2582
|
+
export async function listExpiredAgentInstances(control, input = {}) {
|
|
2583
|
+
const now = input.now ?? nowIso();
|
|
2584
|
+
const staleDeletingBefore = new Date(new Date(now).getTime() - staleDeletingRetrySeconds * 1000).toISOString();
|
|
2585
|
+
const limit = Math.max(1, Math.min(500, input.limit ?? 50));
|
|
2586
|
+
return control.all(`SELECT * FROM ${q("_agent_instances")}
|
|
2587
|
+
WHERE "runtime" = 'ai-chat'
|
|
2588
|
+
AND "triggerType" = 'chat'
|
|
2589
|
+
AND (
|
|
2590
|
+
("expiresAt" IS NOT NULL AND "expiresAt" <= ? AND "status" IN ('active', 'cleanupRequested', 'cleanupError'))
|
|
2591
|
+
OR ("status" = 'deleting' AND "updatedAt" <= ?)
|
|
2592
|
+
)
|
|
2593
|
+
ORDER BY "expiresAt" ASC
|
|
2594
|
+
LIMIT ?`, [now, staleDeletingBefore, limit]);
|
|
2595
|
+
}
|
|
2596
|
+
export async function markAgentCleanupRequested(control, instanceId, input = {}) {
|
|
2597
|
+
const now = input.now ?? nowIso();
|
|
2598
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2599
|
+
SET "status" = CASE WHEN "status" = 'active' THEN 'cleanupRequested' ELSE "status" END,
|
|
2600
|
+
"cleanupRequestedAt" = COALESCE("cleanupRequestedAt", ?),
|
|
2601
|
+
"retentionReason" = COALESCE("retentionReason", ?),
|
|
2602
|
+
"lastError" = ?,
|
|
2603
|
+
"updatedAt" = ?
|
|
2604
|
+
WHERE "id" = ? AND "status" IN ('active', 'cleanupRequested', 'cleanupError')`, [now, input.reason ?? null, input.error ?? null, now, instanceId]);
|
|
2605
|
+
}
|
|
2606
|
+
export async function beginAgentHotCleanup(control, input) {
|
|
2607
|
+
const now = input.now ?? nowIso();
|
|
2608
|
+
const staleDeletingBefore = new Date(new Date(now).getTime() - staleDeletingRetrySeconds * 1000).toISOString();
|
|
2609
|
+
const current = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [input.instance.id]);
|
|
2610
|
+
if (!current)
|
|
2611
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
2612
|
+
if (current.status === "deleted" || current.status === "archived")
|
|
2613
|
+
return { status: "terminal", instance: current };
|
|
2614
|
+
if (current.status === "deleting" && current.updatedAt > staleDeletingBefore)
|
|
2615
|
+
return { status: "busy", instance: current };
|
|
2616
|
+
if (current.status !== "deleting" && !retryableCleanupStatuses.has(current.status))
|
|
2617
|
+
return { status: "busy", instance: current };
|
|
2618
|
+
if (current.status !== "deleting" && (current.expiresAt === null || new Date(current.expiresAt).getTime() > new Date(now).getTime())) {
|
|
2619
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2620
|
+
SET "status" = 'active', "lastError" = NULL, "updatedAt" = ?
|
|
2621
|
+
WHERE "id" = ? AND "status" IN ('cleanupRequested', 'cleanupError')`, [now, current.id]);
|
|
2622
|
+
const rescheduled = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [current.id]);
|
|
2623
|
+
return { status: "reschedule", instance: rescheduled ?? current };
|
|
2624
|
+
}
|
|
2625
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2626
|
+
SET "status" = 'deleting',
|
|
2627
|
+
"cleanupRequestedAt" = COALESCE("cleanupRequestedAt", ?),
|
|
2628
|
+
"retentionReason" = COALESCE("retentionReason", ?),
|
|
2629
|
+
"lastError" = NULL,
|
|
2630
|
+
"updatedAt" = ?
|
|
2631
|
+
WHERE "id" = ?
|
|
2632
|
+
AND (
|
|
2633
|
+
("status" IN ('active', 'cleanupRequested', 'cleanupError') AND "expiresAt" IS NOT NULL AND "expiresAt" <= ?)
|
|
2634
|
+
OR ("status" = 'deleting' AND "updatedAt" <= ?)
|
|
2635
|
+
)`, [now, input.reason ?? null, now, current.id, now, staleDeletingBefore]);
|
|
2636
|
+
const next = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "id" = ? LIMIT 1`, [current.id]);
|
|
2637
|
+
if (!next)
|
|
2638
|
+
throw new AgentHttpError("Agent conversation not found", 404);
|
|
2639
|
+
if (next.status !== "deleting") {
|
|
2640
|
+
if (next.status === "deleted" || next.status === "archived")
|
|
2641
|
+
return { status: "terminal", instance: next };
|
|
2642
|
+
if (next.expiresAt === null || new Date(next.expiresAt).getTime() > new Date(now).getTime() || next.status === "active")
|
|
2643
|
+
return { status: "reschedule", instance: next };
|
|
2644
|
+
return { status: "busy", instance: next };
|
|
2645
|
+
}
|
|
2646
|
+
return { status: "delete", instance: next };
|
|
2647
|
+
}
|
|
2648
|
+
export async function completeAgentHotCleanup(control, instanceId, input = {}) {
|
|
2649
|
+
const now = input.now ?? nowIso();
|
|
2650
|
+
const outcome = input.outcome ?? "deleted";
|
|
2651
|
+
if (outcome === "archived") {
|
|
2652
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2653
|
+
SET "status" = 'archived', "deletedAt" = NULL,
|
|
2654
|
+
"updatedAt" = ?, "lastError" = NULL
|
|
2655
|
+
WHERE "id" = ? AND "status" = 'deleting'
|
|
2656
|
+
AND COALESCE("retentionReason", '') <> 'user-deleted'`, [now, instanceId]);
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2660
|
+
SET "status" = 'deleted', "deletedAt" = COALESCE("deletedAt", ?),
|
|
2661
|
+
"updatedAt" = ?, "lastError" = NULL
|
|
2662
|
+
WHERE "id" = ? AND "status" = 'deleting'
|
|
2663
|
+
AND NOT EXISTS (
|
|
2664
|
+
SELECT 1 FROM ${q("_agent_chapter_purges")} p
|
|
2665
|
+
WHERE p."scopeKind" = 'conversation'
|
|
2666
|
+
AND p."scopeId" = ${q("_agent_instances")}."conversationId"
|
|
2667
|
+
)`, [now, now, instanceId]);
|
|
2668
|
+
}
|
|
2669
|
+
export async function completeAgentUserErasureAfterArtifactDrain(control, userId, input = {}) {
|
|
2670
|
+
const pendingArtifacts = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_chapter_purges")}
|
|
2671
|
+
WHERE "scopeKind" = 'user' AND "scopeId" = ?`, [userId]);
|
|
2672
|
+
if (Number(pendingArtifacts?.count ?? 0) > 0)
|
|
2673
|
+
return false;
|
|
2674
|
+
const pendingRoomRedactions = await countPendingAgentRoomMemberRedactions(control, userId);
|
|
2675
|
+
if (pendingRoomRedactions > 0)
|
|
2676
|
+
return false;
|
|
2677
|
+
const unverifiedHotStorage = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_instances")}
|
|
2678
|
+
WHERE "runtime" = 'ai-chat' AND "triggerType" = 'chat'
|
|
2679
|
+
AND "userId" = ? AND "status" <> 'deleted'
|
|
2680
|
+
AND "hotStorageDeletedAt" IS NULL`, [userId]);
|
|
2681
|
+
if (Number(unverifiedHotStorage?.count ?? 0) > 0)
|
|
2682
|
+
return false;
|
|
2683
|
+
const now = input.now ?? nowIso();
|
|
2684
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2685
|
+
SET "status" = 'deleted',
|
|
2686
|
+
"deletedAt" = COALESCE("deletedAt", ?),
|
|
2687
|
+
"lastError" = NULL,
|
|
2688
|
+
"updatedAt" = ?
|
|
2689
|
+
WHERE "runtime" = 'ai-chat' AND "triggerType" = 'chat'
|
|
2690
|
+
AND "userId" = ? AND "status" <> 'deleted'
|
|
2691
|
+
AND "retentionReason" = 'user-deleted'
|
|
2692
|
+
AND "hotStorageDeletedAt" IS NOT NULL
|
|
2693
|
+
AND NOT EXISTS (
|
|
2694
|
+
SELECT 1 FROM ${q("_agent_chapter_purges")} p
|
|
2695
|
+
WHERE p."scopeKind" = 'user'
|
|
2696
|
+
AND p."scopeId" = ${q("_agent_instances")}."userId"
|
|
2697
|
+
)`, [now, now, userId]);
|
|
2698
|
+
const remaining = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_instances")}
|
|
2699
|
+
WHERE "runtime" = 'ai-chat' AND "triggerType" = 'chat'
|
|
2700
|
+
AND "userId" = ? AND "status" <> 'deleted'`, [userId]);
|
|
2701
|
+
return Number(remaining?.count ?? 0) === 0;
|
|
2702
|
+
}
|
|
2703
|
+
export async function countPendingAgentRoomMemberRedactions(control, userId) {
|
|
2704
|
+
const pending = await control.first(`SELECT COUNT(*) AS "count" FROM ${q("_agent_chapter_purges")}
|
|
2705
|
+
WHERE "scopeKind" = 'room-member'
|
|
2706
|
+
AND substr("scopeId", -(length(?) + 1)) = ':' || ?`, [userId, userId]);
|
|
2707
|
+
return Number(pending?.count ?? 0);
|
|
2708
|
+
}
|
|
2709
|
+
export async function failAgentHotCleanup(control, instanceId, input) {
|
|
2710
|
+
const now = input.now ?? nowIso();
|
|
2711
|
+
await control.run(`UPDATE ${q("_agent_instances")}
|
|
2712
|
+
SET "status" = 'cleanupError', "lastError" = ?, "updatedAt" = ?
|
|
2713
|
+
WHERE "id" = ? AND "status" IN ('cleanupRequested', 'deleting', 'cleanupError')`, [input.error, now, instanceId]);
|
|
2714
|
+
}
|
|
2715
|
+
export async function claimAgentGuideLimits(input) {
|
|
2716
|
+
const limits = input.agent.limits;
|
|
2717
|
+
if (!limits)
|
|
2718
|
+
return;
|
|
2719
|
+
if (input.instance.principalKind === "public") {
|
|
2720
|
+
const publicScope = input.instance.publicScopeHash ?? "public";
|
|
2721
|
+
if (limits.publicIpHourlyRequests || limits.publicIpHourlyOutputTokens) {
|
|
2722
|
+
await claimAgentLimitWindow({
|
|
2723
|
+
control: input.control,
|
|
2724
|
+
scope: "public-ip-hourly",
|
|
2725
|
+
scopeId: publicScope,
|
|
2726
|
+
durationMs: 60 * 60 * 1000,
|
|
2727
|
+
requestLimit: limits.publicIpHourlyRequests,
|
|
2728
|
+
outputLimit: limits.publicIpHourlyOutputTokens,
|
|
2729
|
+
estimatedOutputTokens: input.estimatedOutputTokens
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
if (limits.publicDailyRequests || limits.publicDailyOutputTokens) {
|
|
2733
|
+
await claimAgentLimitWindow({
|
|
2734
|
+
control: input.control,
|
|
2735
|
+
scope: "public-daily",
|
|
2736
|
+
scopeId: publicScope,
|
|
2737
|
+
durationMs: 24 * 60 * 60 * 1000,
|
|
2738
|
+
requestLimit: limits.publicDailyRequests,
|
|
2739
|
+
outputLimit: limits.publicDailyOutputTokens,
|
|
2740
|
+
estimatedOutputTokens: input.estimatedOutputTokens
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
else if (limits.signedInDailyRequests) {
|
|
2745
|
+
await claimAgentLimitWindow({
|
|
2746
|
+
control: input.control,
|
|
2747
|
+
scope: "signed-in-daily",
|
|
2748
|
+
scopeId: input.instance.userId ?? "user",
|
|
2749
|
+
durationMs: 24 * 60 * 60 * 1000,
|
|
2750
|
+
requestLimit: limits.signedInDailyRequests,
|
|
2751
|
+
estimatedOutputTokens: 0
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
/** `limit` unset means unlimited; the window row is still counted so usage stays visible. */
|
|
2756
|
+
export async function claimAgentEntryKickoffLimit(control, instance, limit) {
|
|
2757
|
+
if (instance.principalKind !== "user" || !instance.userId) {
|
|
2758
|
+
throw new AgentHttpError("Agent entry kickoff requires a signed-in user", 403);
|
|
2759
|
+
}
|
|
2760
|
+
await claimAgentLimitWindow({
|
|
2761
|
+
control,
|
|
2762
|
+
scope: "entry-kickoff",
|
|
2763
|
+
scopeId: instance.userId,
|
|
2764
|
+
durationMs: 24 * 60 * 60 * 1000,
|
|
2765
|
+
requestLimit: limit,
|
|
2766
|
+
estimatedOutputTokens: 0
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
/** `limit` unset means unlimited; the window row is still counted so usage stays visible. */
|
|
2770
|
+
export async function claimAgentComposedLimit(control, instance, limit) {
|
|
2771
|
+
if (instance.principalKind !== "user" || !instance.userId) {
|
|
2772
|
+
throw new AgentHttpError("Composed Agent turns require a user conversation", 403);
|
|
2773
|
+
}
|
|
2774
|
+
await claimAgentLimitWindow({
|
|
2775
|
+
control,
|
|
2776
|
+
scope: "composed-daily",
|
|
2777
|
+
scopeId: instance.conversationId,
|
|
2778
|
+
durationMs: 24 * 60 * 60 * 1000,
|
|
2779
|
+
requestLimit: limit,
|
|
2780
|
+
estimatedOutputTokens: 0
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
export async function signAgentToken(secret, claims) {
|
|
2784
|
+
const now = Math.floor(wallNowMs() / 1000);
|
|
2785
|
+
const payload = {
|
|
2786
|
+
...claims,
|
|
2787
|
+
iat: now,
|
|
2788
|
+
exp: now + Math.max(1, Math.floor(claims.ttlSeconds)),
|
|
2789
|
+
nonce: randomId("agent_nonce")
|
|
2790
|
+
};
|
|
2791
|
+
const body = base64url(encoder.encode(JSON.stringify(payload)));
|
|
2792
|
+
const signature = await hmac(secret, body);
|
|
2793
|
+
return `${body}.${signature}`;
|
|
2794
|
+
}
|
|
2795
|
+
export async function verifyAgentToken(secret, token, purpose) {
|
|
2796
|
+
const [body, signature] = token.split(".");
|
|
2797
|
+
// Names the shape, not the verdict: this branch is reached by strings that
|
|
2798
|
+
// are not tokens at all (an absent mint interpolated as "undefined", a
|
|
2799
|
+
// truncated copy/paste), and "Invalid agent token" reads as a refusal of a
|
|
2800
|
+
// token that was actually issued.
|
|
2801
|
+
if (!body || !signature)
|
|
2802
|
+
throw new AgentHttpError("Invalid agent token format", 401);
|
|
2803
|
+
const expected = await hmac(secret, body);
|
|
2804
|
+
if (!timingSafeEqual(signature, expected))
|
|
2805
|
+
throw new AgentHttpError("Invalid agent token signature", 401);
|
|
2806
|
+
let claims;
|
|
2807
|
+
try {
|
|
2808
|
+
claims = JSON.parse(decoder.decode(fromBase64url(body)));
|
|
2809
|
+
}
|
|
2810
|
+
catch {
|
|
2811
|
+
throw new AgentHttpError("Invalid agent token payload", 401);
|
|
2812
|
+
}
|
|
2813
|
+
if (claims.v !== 1 || claims.aud !== "raybase-agent-chat" || claims.purpose !== purpose)
|
|
2814
|
+
throw new AgentHttpError("Invalid agent token claims", 401);
|
|
2815
|
+
if (claims.exp <= Math.floor(wallNowMs() / 1000))
|
|
2816
|
+
throw new AgentHttpError("Agent token expired", 401);
|
|
2817
|
+
return claims;
|
|
2818
|
+
}
|
|
2819
|
+
export function agentTokenClaims(input) {
|
|
2820
|
+
return {
|
|
2821
|
+
v: 1,
|
|
2822
|
+
purpose: input.purpose,
|
|
2823
|
+
aud: "raybase-agent-chat",
|
|
2824
|
+
agentName: input.instance.agentName,
|
|
2825
|
+
conversationId: input.instance.conversationId,
|
|
2826
|
+
runtime: "ai-chat",
|
|
2827
|
+
triggerType: "chat",
|
|
2828
|
+
principalKind: input.instance.principalKind,
|
|
2829
|
+
userId: input.userId ?? input.instance.userId ?? undefined,
|
|
2830
|
+
publicScopeHash: input.instance.publicScopeHash ?? undefined,
|
|
2831
|
+
organizationId: input.instance.organizationId ?? undefined,
|
|
2832
|
+
route: input.instance.route ?? undefined,
|
|
2833
|
+
surface: input.instance.surface,
|
|
2834
|
+
socketMode: input.socketMode,
|
|
2835
|
+
ttlSeconds: input.ttlSeconds ?? (input.purpose === "agent-websocket" ? websocketBootstrapTtlSeconds : defaultConversationTtlSeconds)
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
export function assertAgentTokenMatchesInstance(claims, instance) {
|
|
2839
|
+
if (claims.agentName !== instance.agentName || claims.conversationId !== instance.conversationId || claims.runtime !== instance.runtime) {
|
|
2840
|
+
throw new AgentHttpError("Agent token does not match conversation", 401);
|
|
2841
|
+
}
|
|
2842
|
+
if (claims.principalKind !== instance.principalKind)
|
|
2843
|
+
throw new AgentHttpError("Agent token principal mismatch", 401);
|
|
2844
|
+
if (instance.kind !== "room") {
|
|
2845
|
+
if ((claims.userId ?? null) !== (instance.userId ?? null))
|
|
2846
|
+
throw new AgentHttpError("Agent token user mismatch", 401);
|
|
2847
|
+
if ((claims.publicScopeHash ?? null) !== (instance.publicScopeHash ?? null))
|
|
2848
|
+
throw new AgentHttpError("Agent token public scope mismatch", 401);
|
|
2849
|
+
}
|
|
2850
|
+
else if (!claims.userId) {
|
|
2851
|
+
throw new AgentHttpError("Agent token user mismatch", 401);
|
|
2852
|
+
}
|
|
2853
|
+
if ((claims.organizationId ?? null) !== (instance.organizationId ?? null))
|
|
2854
|
+
throw new AgentHttpError("Agent token organization mismatch", 401);
|
|
2855
|
+
}
|
|
2856
|
+
export async function assertAgentTokenConversationAccess(control, claims, instance) {
|
|
2857
|
+
assertAgentTokenMatchesInstance(claims, instance);
|
|
2858
|
+
if (instance.kind !== "room")
|
|
2859
|
+
return { membership: null };
|
|
2860
|
+
const membership = await findAgentRoomMember(control, instance.conversationId, claims.userId);
|
|
2861
|
+
if (!membership || membership.status !== "active") {
|
|
2862
|
+
throw new AgentHttpError("Agent room member mismatch", 403);
|
|
2863
|
+
}
|
|
2864
|
+
return { membership };
|
|
2865
|
+
}
|
|
2866
|
+
export function extractBearerToken(request) {
|
|
2867
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
2868
|
+
const match = /^Bearer\s+(.+)$/i.exec(authorization.trim());
|
|
2869
|
+
return match?.[1] ?? null;
|
|
2870
|
+
}
|
|
2871
|
+
export function raybaseActionToAiTool(action, contextFactory) {
|
|
2872
|
+
return tool({
|
|
2873
|
+
description: action.description,
|
|
2874
|
+
inputSchema: jsonSchema(actionInputJsonSchema(action)),
|
|
2875
|
+
execute: async (input, options = {}) => {
|
|
2876
|
+
const toolCallId = typeof options?.toolCallId === "string" ? options.toolCallId : randomId("tool_call");
|
|
2877
|
+
const ctx = await contextFactory({ toolCallId });
|
|
2878
|
+
return executeRaybaseAgentAction(action, input, ctx);
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
export async function executeRaybaseAgentAction(action, rawInput,
|
|
2883
|
+
// Keep raw control on this wrapper only; action handlers receive it as
|
|
2884
|
+
// ctx.control through executeAction().
|
|
2885
|
+
ctx) {
|
|
2886
|
+
// The action sees the agent as its principal. Authority (policies, audience,
|
|
2887
|
+
// data access) still comes from the run's own user or visitor, which the
|
|
2888
|
+
// agent principal carries as onBehalfOf; what changes is attribution.
|
|
2889
|
+
const principal = agentPrincipal({
|
|
2890
|
+
agent: ctx.agentName,
|
|
2891
|
+
onBehalfOf: ctx.principal,
|
|
2892
|
+
scopes: ctx.agentScopes,
|
|
2893
|
+
organizationId: ctx.organizationId ?? null,
|
|
2894
|
+
runId: ctx.runId,
|
|
2895
|
+
conversationId: ctx.conversationId
|
|
2896
|
+
});
|
|
2897
|
+
// Tool arguments arrive in the strict wire form a provider will carry. The
|
|
2898
|
+
// action's own schema is the one that decides, so the padding comes off
|
|
2899
|
+
// first; see restoreStrictActionInput.
|
|
2900
|
+
return executeAction(action, restoreStrictActionInput(action, rawInput), {
|
|
2901
|
+
request: ctx.request,
|
|
2902
|
+
env: ctx.env,
|
|
2903
|
+
user: ctx.user,
|
|
2904
|
+
principal,
|
|
2905
|
+
db: ctx.db,
|
|
2906
|
+
auth: ctx.control
|
|
2907
|
+
? createRaybaseAuthContextRuntime({
|
|
2908
|
+
db: ctx.control,
|
|
2909
|
+
secret: authSecret(ctx.env),
|
|
2910
|
+
config: ctx.config,
|
|
2911
|
+
sessionAuthenticated: false,
|
|
2912
|
+
principal,
|
|
2913
|
+
request: ctx.request,
|
|
2914
|
+
audit: ctx.audit
|
|
2915
|
+
})
|
|
2916
|
+
: unsupportedRaybaseAuthContextRuntime,
|
|
2917
|
+
ai: ctx.ai,
|
|
2918
|
+
settings: ctx.settings,
|
|
2919
|
+
notifications: ctx.notifications,
|
|
2920
|
+
jobs: ctx.jobs,
|
|
2921
|
+
agentName: ctx.agentName,
|
|
2922
|
+
agentInstanceId: ctx.agentInstanceId,
|
|
2923
|
+
trigger: ctx.trigger,
|
|
2924
|
+
conversationId: ctx.conversationId,
|
|
2925
|
+
...(ctx.conversations ? { conversations: ctx.conversations } : {}),
|
|
2926
|
+
runId: ctx.runId,
|
|
2927
|
+
requestId: ctx.requestId,
|
|
2928
|
+
toolCallId: ctx.toolCallId,
|
|
2929
|
+
route: ctx.route,
|
|
2930
|
+
surface: ctx.surface,
|
|
2931
|
+
organizationId: ctx.organizationId,
|
|
2932
|
+
audit: ctx.audit,
|
|
2933
|
+
agents: ctx.agents ?? unsupportedRaybaseAgentInvocationRuntime,
|
|
2934
|
+
control: ctx.control
|
|
2935
|
+
}, {
|
|
2936
|
+
surface: "agent",
|
|
2937
|
+
requirePolicy: ctx.principal.kind === "user",
|
|
2938
|
+
onAccounting: async (event) => {
|
|
2939
|
+
if (!ctx.control)
|
|
2940
|
+
return;
|
|
2941
|
+
await recordAiActionEvent(ctx.control, {
|
|
2942
|
+
userId: event.status === "denied" && ctx.principal.kind === "user" ? ctx.principal.user.id : actionEventUserId(ctx.principal),
|
|
2943
|
+
agent: ctx.agentName,
|
|
2944
|
+
conversationId: ctx.conversationId,
|
|
2945
|
+
actionName: event.actionName,
|
|
2946
|
+
actionInput: event.actionInput,
|
|
2947
|
+
status: event.status,
|
|
2948
|
+
error: event.error
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
export function agentLimits(config, agent) {
|
|
2954
|
+
const defaults = normalizeAiDefaults(config);
|
|
2955
|
+
const aiLimits = normalizeAiLimits(config);
|
|
2956
|
+
return {
|
|
2957
|
+
maxInputChars: agent.limits?.maxInputChars ?? defaults.maxInputChars,
|
|
2958
|
+
maxOutputTokens: agent.limits?.maxOutputTokens ?? defaults.maxOutputTokens,
|
|
2959
|
+
maxToolCallsPerTurn: agent.limits?.maxToolCallsPerTurn ?? aiLimits.maxToolCallsPerTurn,
|
|
2960
|
+
maxLoopStepsPerTurn: agent.limits?.maxLoopStepsPerTurn ?? aiLimits.maxLoopStepsPerTurn,
|
|
2961
|
+
maxConversationMessages: agent.limits?.maxConversationMessages ?? aiLimits.maxConversationMessages,
|
|
2962
|
+
loopReserveChars: agent.limits?.loopReserveChars,
|
|
2963
|
+
publicIpHourlyRequests: agent.limits?.publicIpHourlyRequests,
|
|
2964
|
+
publicIpHourlyOutputTokens: agent.limits?.publicIpHourlyOutputTokens,
|
|
2965
|
+
publicDailyRequests: agent.limits?.publicDailyRequests ?? aiLimits.perPublicDailyRequests,
|
|
2966
|
+
publicDailyOutputTokens: agent.limits?.publicDailyOutputTokens ?? aiLimits.perPublicDailyOutputTokens,
|
|
2967
|
+
signedInDailyRequests: agent.limits?.signedInDailyRequests ?? aiLimits.perUserDailyRequests,
|
|
2968
|
+
composedPerDay: agent.limits?.composedPerDay,
|
|
2969
|
+
entryKickoffPerDay: agent.limits?.entryKickoffPerDay
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
export function agentCompaction(agent) {
|
|
2973
|
+
return {
|
|
2974
|
+
enabled: agent.compaction?.enabled ?? defaultAgentCompaction.enabled,
|
|
2975
|
+
model: agent.compaction?.model,
|
|
2976
|
+
keepRecentMessages: agent.compaction?.keepRecentMessages ?? defaultAgentCompaction.keepRecentMessages,
|
|
2977
|
+
triggerRatio: agent.compaction?.triggerRatio ?? defaultAgentCompaction.triggerRatio,
|
|
2978
|
+
minSummaryOutputTokens: agent.compaction?.minSummaryOutputTokens ?? defaultAgentCompaction.minSummaryOutputTokens,
|
|
2979
|
+
summarySpanRatio: agent.compaction?.summarySpanRatio ?? defaultAgentCompaction.summarySpanRatio,
|
|
2980
|
+
maxSummaryOutputTokens: agent.compaction?.maxSummaryOutputTokens ?? defaultAgentCompaction.maxSummaryOutputTokens
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Output-token budget for one chapter-close summarization call, proportional
|
|
2985
|
+
* to the span being summarized:
|
|
2986
|
+
* clamp(minSummaryOutputTokens, round(summarySpanRatio × spanChars/4),
|
|
2987
|
+
* maxSummaryOutputTokens). `spanChars` is the total content length of the
|
|
2988
|
+
* messages actually being closed, so a three-message exchange keeps the floor
|
|
2989
|
+
* while a twenty-turn working session gets room for its settled facts.
|
|
2990
|
+
* The ceiling wins over the floor when a pinned `maxSummaryOutputTokens`
|
|
2991
|
+
* sits below the (defaulted) floor: an app that pinned the old flat budget
|
|
2992
|
+
* keeps exactly its pre-proportional summaries.
|
|
2993
|
+
*/
|
|
2994
|
+
export function agentSummaryBudgetTokens(compaction, spanChars) {
|
|
2995
|
+
const min = compaction?.minSummaryOutputTokens ?? defaultAgentCompaction.minSummaryOutputTokens;
|
|
2996
|
+
const ratio = compaction?.summarySpanRatio ?? defaultAgentCompaction.summarySpanRatio;
|
|
2997
|
+
const max = compaction?.maxSummaryOutputTokens ?? defaultAgentCompaction.maxSummaryOutputTokens;
|
|
2998
|
+
const proportional = Math.round(ratio * estimateTokensFromChars(spanChars));
|
|
2999
|
+
return Math.min(max, Math.max(min, proportional));
|
|
3000
|
+
}
|
|
3001
|
+
export function agentModel(config, agent, runtimeDefaultModel) {
|
|
3002
|
+
return agent.model ?? runtimeDefaultModel ?? normalizeAiDefaults(config).model;
|
|
3003
|
+
}
|
|
3004
|
+
export function actionEventUserId(principal) {
|
|
3005
|
+
return principal.kind === "user" ? principal.user.id : principal.id;
|
|
3006
|
+
}
|
|
3007
|
+
async function claimAgentLimitWindow(input) {
|
|
3008
|
+
const now = appNowMs();
|
|
3009
|
+
const windowStartMs = Math.floor(now / input.durationMs) * input.durationMs;
|
|
3010
|
+
const windowStart = new Date(windowStartMs).toISOString();
|
|
3011
|
+
const windowEnd = new Date(windowStartMs + input.durationMs).toISOString();
|
|
3012
|
+
const row = await input.control.first(`SELECT "requestCount", "estimatedOutputTokens" FROM ${q("_agent_limit_windows")}
|
|
3013
|
+
WHERE "scope" = ? AND "scopeId" = ? AND "windowStart" = ? LIMIT 1`, [input.scope, input.scopeId, windowStart]);
|
|
3014
|
+
const nextRequests = Number(row?.requestCount ?? 0) + 1;
|
|
3015
|
+
const nextOutput = Number(row?.estimatedOutputTokens ?? 0) + Math.max(0, input.estimatedOutputTokens);
|
|
3016
|
+
if (input.requestLimit !== undefined && nextRequests > input.requestLimit) {
|
|
3017
|
+
throw new AgentHttpError(`Agent request limit exceeded for ${input.scope}`, 429);
|
|
3018
|
+
}
|
|
3019
|
+
if (input.outputLimit !== undefined && nextOutput > input.outputLimit) {
|
|
3020
|
+
throw new AgentHttpError(`Agent output token limit exceeded for ${input.scope}`, 429);
|
|
3021
|
+
}
|
|
3022
|
+
if (row) {
|
|
3023
|
+
await input.control.run(`UPDATE ${q("_agent_limit_windows")}
|
|
3024
|
+
SET "requestCount" = ?, "estimatedOutputTokens" = ?, "updatedAt" = ?
|
|
3025
|
+
WHERE "scope" = ? AND "scopeId" = ? AND "windowStart" = ?`, [nextRequests, nextOutput, nowIso(), input.scope, input.scopeId, windowStart]);
|
|
3026
|
+
}
|
|
3027
|
+
else {
|
|
3028
|
+
await input.control.run(`INSERT INTO ${q("_agent_limit_windows")}
|
|
3029
|
+
("id", "scope", "scopeId", "windowStart", "windowEnd", "requestCount", "estimatedOutputTokens", "updatedAt")
|
|
3030
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [randomId("agent_limit"), input.scope, input.scopeId, windowStart, windowEnd, nextRequests, nextOutput, nowIso()]);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
export async function assertAgentConversationAccess(input) {
|
|
3034
|
+
if (input.instance.kind !== "room") {
|
|
3035
|
+
assertAgentInstanceAccess(input.instance, input.access);
|
|
3036
|
+
return { membership: null };
|
|
3037
|
+
}
|
|
3038
|
+
if (input.access.principal.kind !== "user") {
|
|
3039
|
+
throw new AgentHttpError("Agent room member mismatch", 403);
|
|
3040
|
+
}
|
|
3041
|
+
const membership = await findAgentRoomMember(input.control, input.instance.conversationId, input.access.principal.user.id);
|
|
3042
|
+
if (!membership || membership.status !== "active") {
|
|
3043
|
+
throw new AgentHttpError("Agent room member mismatch", 403);
|
|
3044
|
+
}
|
|
3045
|
+
if (input.require === "owner" && membership.role !== "owner") {
|
|
3046
|
+
throw new AgentHttpError("Agent conversation owner mismatch", 403);
|
|
3047
|
+
}
|
|
3048
|
+
return { membership };
|
|
3049
|
+
}
|
|
3050
|
+
function assertAgentInstanceAccess(instance, access) {
|
|
3051
|
+
if (instance.principalKind !== access.principal.kind)
|
|
3052
|
+
throw new AgentHttpError("Agent conversation principal mismatch", 403);
|
|
3053
|
+
if (access.principal.kind === "user" && instance.userId !== access.principal.user.id)
|
|
3054
|
+
throw new AgentHttpError("Agent conversation user mismatch", 403);
|
|
3055
|
+
if (access.principal.kind === "public" && instance.publicScopeHash !== access.principal.id)
|
|
3056
|
+
throw new AgentHttpError("Agent conversation public scope mismatch", 403);
|
|
3057
|
+
}
|
|
3058
|
+
function uniqueAudiences(values) {
|
|
3059
|
+
return Array.from(new Set(values));
|
|
3060
|
+
}
|
|
3061
|
+
function normalizeRoute(path) {
|
|
3062
|
+
const value = path.startsWith("/") ? path : `/${path}`;
|
|
3063
|
+
return value.replace(/\/{2,}/g, "/") || "/";
|
|
3064
|
+
}
|
|
3065
|
+
function optionalUserField(user, field) {
|
|
3066
|
+
if (!user || typeof user !== "object")
|
|
3067
|
+
return undefined;
|
|
3068
|
+
const value = user[field];
|
|
3069
|
+
return typeof value === "string" && value ? value : undefined;
|
|
3070
|
+
}
|
|
3071
|
+
function principalOrganizationId(user) {
|
|
3072
|
+
return optionalUserField(user, "organizationId") ?? null;
|
|
3073
|
+
}
|
|
3074
|
+
async function publicAgentPrincipal(request, organizationId, requestContext) {
|
|
3075
|
+
const ip = request.headers.get("cf-connecting-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "local";
|
|
3076
|
+
const userAgent = request.headers.get("user-agent") ?? "unknown";
|
|
3077
|
+
const sessionHash = await sha256Hex(`${ip}:${userAgent}:${requestContext.surface}`);
|
|
3078
|
+
return {
|
|
3079
|
+
kind: "public",
|
|
3080
|
+
id: sessionHash.slice(0, 64),
|
|
3081
|
+
sessionHash: sessionHash.slice(0, 64),
|
|
3082
|
+
organizationId: organizationId ?? undefined,
|
|
3083
|
+
surface: requestContext.surface,
|
|
3084
|
+
route: requestContext.route,
|
|
3085
|
+
origin: requestContext.origin,
|
|
3086
|
+
ipHash: (await sha256Hex(ip)).slice(0, 64),
|
|
3087
|
+
userAgentHash: (await sha256Hex(userAgent)).slice(0, 64)
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
function base64url(bytes) {
|
|
3091
|
+
let binary = "";
|
|
3092
|
+
for (const byte of bytes)
|
|
3093
|
+
binary += String.fromCharCode(byte);
|
|
3094
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3095
|
+
}
|
|
3096
|
+
function fromBase64url(value) {
|
|
3097
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
3098
|
+
const binary = atob(padded);
|
|
3099
|
+
const bytes = new Uint8Array(binary.length);
|
|
3100
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
3101
|
+
bytes[index] = binary.charCodeAt(index);
|
|
3102
|
+
return bytes;
|
|
3103
|
+
}
|
|
3104
|
+
async function hmac(secret, data) {
|
|
3105
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
3106
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
3107
|
+
return base64url(new Uint8Array(signature));
|
|
3108
|
+
}
|
|
3109
|
+
function timingSafeEqual(left, right) {
|
|
3110
|
+
const a = encoder.encode(left);
|
|
3111
|
+
const b = encoder.encode(right);
|
|
3112
|
+
if (a.length !== b.length)
|
|
3113
|
+
return false;
|
|
3114
|
+
let result = 0;
|
|
3115
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
3116
|
+
result |= (a[index] ?? 0) ^ (b[index] ?? 0);
|
|
3117
|
+
}
|
|
3118
|
+
return result === 0;
|
|
3119
|
+
}
|
|
3120
|
+
export function jsonSchemaObject(schema) {
|
|
3121
|
+
return schema;
|
|
3122
|
+
}
|
|
3123
|
+
//# sourceMappingURL=agent.js.map
|