spine-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* @module agent-runner
|
|
4
|
+
* @audience both
|
|
5
|
+
* @layer shared-core
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* AI agent inference orchestrator with RAG, tool dispatch, and confidence-based
|
|
9
|
+
* escalation. All agent behavior is defined via JSONB configuration stored in
|
|
10
|
+
* existing schema tables — no dedicated migrations are needed.
|
|
11
|
+
*
|
|
12
|
+
* Configuration resolution chain (highest to lowest priority):
|
|
13
|
+
* 1. `thread.data.agent_id` → `thread.type.design_schema.default_agent_id`
|
|
14
|
+
* 2. `thread.data.prompt_config_id` → `agent.metadata.default_prompt_config_id`
|
|
15
|
+
*
|
|
16
|
+
* Inference loop (per `runAgent` call):
|
|
17
|
+
* 1. `resolveAgentConfig` — resolve agent + prompt_config from thread
|
|
18
|
+
* 2. Save user message to `messages` table
|
|
19
|
+
* 3. `executeAgentInference` — iterative tool-call loop (max 5 iterations):
|
|
20
|
+
* a. `buildContext` — assemble system prompt + RAG + history + tools
|
|
21
|
+
* b. `callInference` — call OpenAI-compatible API (or return mock)
|
|
22
|
+
* c. `dispatchTools` — execute any tool_calls via actions table
|
|
23
|
+
* d. Rebuild context with tool results and repeat
|
|
24
|
+
* 4. Confidence check — if below threshold, `handleEscalation`
|
|
25
|
+
* 5. Save agent response to `messages` table
|
|
26
|
+
* 6. Emit `agent.inference.completed` audit log
|
|
27
|
+
*
|
|
28
|
+
* Environment variables used by `callInference`:
|
|
29
|
+
* - `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `LLM_API_KEY` — LLM auth
|
|
30
|
+
* - `OPENAI_BASE_URL` / `LLM_BASE_URL` — API base URL (default: OpenAI)
|
|
31
|
+
* - `LLM_DEFAULT_MODEL` — model name fallback (default: 'gpt-4o')
|
|
32
|
+
*
|
|
33
|
+
* INVARIANT: if no API key is set, `callInference` returns a mock response
|
|
34
|
+
* instead of throwing — safe for local development without credentials.
|
|
35
|
+
* INVARIANT: `runAgent` throws on critical failures (config missing, inference
|
|
36
|
+
* error) — callers must handle the rejection.
|
|
37
|
+
* INVARIANT: `resolveAgentConfig` returns null (not throws) on missing config;
|
|
38
|
+
* `runAgent` converts this to a throw.
|
|
39
|
+
*
|
|
40
|
+
* @seeAlso pipeline-runner.ts (tool dispatch calls runPipeline for run_pipeline tool)
|
|
41
|
+
* @seeAlso audit.ts (emitAudit for agent.inference.* events)
|
|
42
|
+
* @seeAlso index.ts (runAgent, resolveAgentConfig re-exported)
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { CoreContext } from './middleware'
|
|
46
|
+
import { adminDb } from './db'
|
|
47
|
+
import { emitAudit } from './audit'
|
|
48
|
+
|
|
49
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolved agent configuration bundle. Output of `resolveAgentConfig`.
|
|
53
|
+
* Passed to `executeAgentInference` and `buildContext`.
|
|
54
|
+
*
|
|
55
|
+
* @outputSpec agent: ai_agents row (system_prompt, model_config, metadata)
|
|
56
|
+
* @outputSpec promptConfig: prompt_configs row (context_template,
|
|
57
|
+
* knowledge_sources, available_tools, confidence_threshold, escalation_*)
|
|
58
|
+
* @outputSpec thread: threads row with joined type record
|
|
59
|
+
* @outputSpec threadType: types row (design_schema.default_agent_id)
|
|
60
|
+
*/
|
|
61
|
+
export interface AgentConfig {
|
|
62
|
+
agent: any
|
|
63
|
+
promptConfig: any
|
|
64
|
+
thread: any
|
|
65
|
+
threadType: any
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Structured result from a single LLM inference call.
|
|
70
|
+
*
|
|
71
|
+
* @outputSpec content: string — agent response text
|
|
72
|
+
* @outputSpec confidence: number — 0-1 score; derived from logprobs or 0.85 default
|
|
73
|
+
* @outputSpec tool_calls: ToolCall[] | undefined — tools the model wants to call
|
|
74
|
+
* @outputSpec metadata: { model, usage, finish_reason } | undefined
|
|
75
|
+
*/
|
|
76
|
+
export interface InferenceResult {
|
|
77
|
+
content: string
|
|
78
|
+
confidence: number
|
|
79
|
+
tool_calls?: ToolCall[]
|
|
80
|
+
metadata?: Record<string, any>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A single tool invocation requested by the LLM in an `InferenceResult`.
|
|
85
|
+
*
|
|
86
|
+
* @inputSpec tool: string — action.slug to look up in the actions table
|
|
87
|
+
* @inputSpec params: Record<string, any> — parsed from OpenAI function call arguments
|
|
88
|
+
* @inputSpec id: string — opaque tool_call ID from the LLM response
|
|
89
|
+
*/
|
|
90
|
+
export interface ToolCall {
|
|
91
|
+
tool: string
|
|
92
|
+
params: Record<string, any>
|
|
93
|
+
id: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Result of executing a single `ToolCall`.
|
|
98
|
+
*
|
|
99
|
+
* @outputSpec tool: string — mirrors ToolCall.tool
|
|
100
|
+
* @outputSpec result: any — handler return value on success; null on error
|
|
101
|
+
* @outputSpec error: string | undefined — error message if execution failed
|
|
102
|
+
* @outputSpec id: string — mirrors ToolCall.id for correlation
|
|
103
|
+
*/
|
|
104
|
+
export interface ToolResult {
|
|
105
|
+
tool: string
|
|
106
|
+
result: any
|
|
107
|
+
error?: string
|
|
108
|
+
id: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── PRIMARY EXPORTS ────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Main entry point: run a full agent inference cycle for a user message.
|
|
115
|
+
*
|
|
116
|
+
* Saves the user message, runs the inference loop (with tool calls), checks
|
|
117
|
+
* confidence, saves the agent response, and emits audit logs.
|
|
118
|
+
*
|
|
119
|
+
* @param threadId - UUID of the thread to run inference on
|
|
120
|
+
* @param userMessage - Raw message text from the user
|
|
121
|
+
* @param ctx - CoreContext with accountId, principal, requestId
|
|
122
|
+
* @returns Promise<any> — the saved agent message record from the `messages` table
|
|
123
|
+
* @throws Error — if agent config cannot be resolved, or LLM inference fails
|
|
124
|
+
* @inputSpec threadId: string — valid UUID in threads table
|
|
125
|
+
* @inputSpec userMessage: string — non-empty message text
|
|
126
|
+
* @inputSpec ctx.accountId: string | null — used to scope DB lookups
|
|
127
|
+
* @outputSpec messages row — the inserted agent reply with content and metadata
|
|
128
|
+
* @sideEffects DB write: inserts 2 messages rows (user + agent)
|
|
129
|
+
* @sideEffects DB write: emitAudit (agent.inference.completed or agent.inference.failed)
|
|
130
|
+
* @sideEffects HTTP call: callInference (LLM API)
|
|
131
|
+
* @sideEffects DB write (conditional): handleEscalation if confidence < threshold
|
|
132
|
+
* @calledBy functions/ai-agents.ts handler for POST ?action=run
|
|
133
|
+
* @calledBy v2-custom/ import callers
|
|
134
|
+
* @calls resolveAgentConfig, saveMessage, executeAgentInference,
|
|
135
|
+
* handleEscalation, emitAudit
|
|
136
|
+
* @testUnit tests/unit/agent-runner.test.ts
|
|
137
|
+
* @testIntegration tests/integration/agent-runner.test.ts
|
|
138
|
+
*
|
|
139
|
+
* @example API handler usage
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { runAgent } from './_shared/index'
|
|
142
|
+
* const agentMsg = await runAgent(body.thread_id, body.message, ctx)
|
|
143
|
+
* return agentMsg
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export async function runAgent(
|
|
147
|
+
threadId: string,
|
|
148
|
+
userMessage: string,
|
|
149
|
+
ctx: CoreContext
|
|
150
|
+
): Promise<any> {
|
|
151
|
+
const startTime = Date.now()
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// 1. Resolve configurations (thread → agent → prompt config)
|
|
155
|
+
const config = await resolveAgentConfig(threadId, ctx)
|
|
156
|
+
if (!config) {
|
|
157
|
+
throw new Error(`Could not resolve agent configuration for thread ${threadId}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { agent, promptConfig, thread, threadType } = config
|
|
161
|
+
|
|
162
|
+
// 2. Save user message to thread
|
|
163
|
+
const userMsg = await saveMessage(threadId, userMessage, 'human', null, ctx)
|
|
164
|
+
|
|
165
|
+
// 3. Execute agent with full context
|
|
166
|
+
const agentResponse = await executeAgentInference(
|
|
167
|
+
config,
|
|
168
|
+
userMessage,
|
|
169
|
+
ctx
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// 4. Check confidence and escalate if needed
|
|
173
|
+
if (agentResponse.confidence < (promptConfig.confidence_threshold || 0)) {
|
|
174
|
+
await handleEscalation(
|
|
175
|
+
threadId,
|
|
176
|
+
agentResponse.confidence,
|
|
177
|
+
promptConfig,
|
|
178
|
+
ctx
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 5. Save agent response
|
|
183
|
+
const agentMsg = await saveMessage(
|
|
184
|
+
threadId,
|
|
185
|
+
agentResponse.content,
|
|
186
|
+
'agent',
|
|
187
|
+
{
|
|
188
|
+
confidence: agentResponse.confidence,
|
|
189
|
+
tool_calls: agentResponse.tool_calls,
|
|
190
|
+
agent_id: agent.id,
|
|
191
|
+
prompt_config_id: promptConfig.id
|
|
192
|
+
},
|
|
193
|
+
ctx
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// 6. Emit audit log
|
|
197
|
+
await emitAudit(ctx, 'agent.inference.completed', {
|
|
198
|
+
type: 'agent_message',
|
|
199
|
+
id: agentMsg.id,
|
|
200
|
+
account_id: ctx.accountId ?? undefined
|
|
201
|
+
}, {
|
|
202
|
+
thread_id: threadId,
|
|
203
|
+
agent_id: agent.id,
|
|
204
|
+
confidence: agentResponse.confidence,
|
|
205
|
+
has_tool_calls: !!agentResponse.tool_calls?.length,
|
|
206
|
+
duration_ms: Date.now() - startTime
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return agentMsg
|
|
210
|
+
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
// Log failure
|
|
213
|
+
await emitAudit(ctx, 'agent.inference.failed', {
|
|
214
|
+
type: 'agent_message',
|
|
215
|
+
id: 'failed',
|
|
216
|
+
account_id: ctx.accountId ?? undefined
|
|
217
|
+
}, {
|
|
218
|
+
thread_id: threadId,
|
|
219
|
+
error: error.message
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
throw error
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolves the agent, prompt config, thread, and thread type for a given thread.
|
|
228
|
+
*
|
|
229
|
+
* Resolution priority:
|
|
230
|
+
* - `agent_id`: `thread.data.agent_id` → `thread.type.design_schema.default_agent_id`
|
|
231
|
+
* - `prompt_config_id`: `thread.data.prompt_config_id` →
|
|
232
|
+
* `agent.metadata.default_prompt_config_id`
|
|
233
|
+
*
|
|
234
|
+
* Returns `null` (does not throw) when any required record is missing. `runAgent`
|
|
235
|
+
* converts a null return to a thrown Error.
|
|
236
|
+
*
|
|
237
|
+
* @param threadId - UUID of the thread
|
|
238
|
+
* @param ctx - CoreContext (requestId used for error logging)
|
|
239
|
+
* @returns Promise<AgentConfig | null> — null if config cannot be resolved
|
|
240
|
+
* @throws never — errors logged to console, returns null
|
|
241
|
+
* @inputSpec threadId: string — valid UUID in threads table
|
|
242
|
+
* @outputSpec AgentConfig | null
|
|
243
|
+
* @sideEffects DB reads: threads (with type join), ai_agents, prompt_configs
|
|
244
|
+
* @calledBy runAgent
|
|
245
|
+
* @testUnit tests/unit/agent-runner.test.ts — 'resolveAgentConfig'
|
|
246
|
+
*/
|
|
247
|
+
export async function resolveAgentConfig(
|
|
248
|
+
threadId: string,
|
|
249
|
+
ctx: CoreContext
|
|
250
|
+
): Promise<AgentConfig | null> {
|
|
251
|
+
// Load thread with its type
|
|
252
|
+
const { data: thread, error: threadError } = await adminDb
|
|
253
|
+
.from('threads')
|
|
254
|
+
.select('*, type:types(*)')
|
|
255
|
+
.eq('id', threadId)
|
|
256
|
+
.single()
|
|
257
|
+
|
|
258
|
+
if (threadError || !thread) {
|
|
259
|
+
console.error(`Thread not found: ${threadId}`, threadError)
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const threadType = thread.type
|
|
264
|
+
const threadData = thread.data || {}
|
|
265
|
+
|
|
266
|
+
// Resolve agent_id: thread.data > thread.type.design_schema.default_agent_id
|
|
267
|
+
let agentId = threadData.agent_id
|
|
268
|
+
if (!agentId && threadType?.design_schema?.default_agent_id) {
|
|
269
|
+
agentId = threadType.design_schema.default_agent_id
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!agentId) {
|
|
273
|
+
console.error(`No agent assigned to thread ${threadId}`)
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Load agent
|
|
278
|
+
const { data: agent, error: agentError } = await adminDb
|
|
279
|
+
.from('ai_agents')
|
|
280
|
+
.select('*')
|
|
281
|
+
.eq('id', agentId)
|
|
282
|
+
.eq('is_active', true)
|
|
283
|
+
.single()
|
|
284
|
+
|
|
285
|
+
if (agentError || !agent) {
|
|
286
|
+
console.error(`Agent not found: ${agentId}`, agentError)
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const agentMetadata = agent.metadata || {}
|
|
291
|
+
|
|
292
|
+
// Resolve prompt_config_id: thread.data > agent.metadata.default_prompt_config_id
|
|
293
|
+
let promptConfigId = threadData.prompt_config_id
|
|
294
|
+
if (!promptConfigId && agentMetadata.default_prompt_config_id) {
|
|
295
|
+
promptConfigId = agentMetadata.default_prompt_config_id
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!promptConfigId) {
|
|
299
|
+
console.error(`No prompt config for thread ${threadId}`)
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Load prompt config
|
|
304
|
+
const { data: promptConfig, error: configError } = await adminDb
|
|
305
|
+
.from('prompt_configs')
|
|
306
|
+
.select('*')
|
|
307
|
+
.eq('id', promptConfigId)
|
|
308
|
+
.eq('is_active', true)
|
|
309
|
+
.single()
|
|
310
|
+
|
|
311
|
+
if (configError || !promptConfig) {
|
|
312
|
+
console.error(`Prompt config not found: ${promptConfigId}`, configError)
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { agent, promptConfig, thread, threadType }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── INFERENCE LOOP ─────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Runs the iterative inference loop: build context → call LLM → dispatch tools
|
|
323
|
+
* → rebuild context → repeat (up to `maxToolIterations`).
|
|
324
|
+
*
|
|
325
|
+
* Returns early when the LLM response has no tool_calls. Stops on last
|
|
326
|
+
* iteration with a note appended if any tool failed.
|
|
327
|
+
*
|
|
328
|
+
* @param config - Resolved AgentConfig
|
|
329
|
+
* @param userMessage - Original user message text
|
|
330
|
+
* @param ctx - CoreContext
|
|
331
|
+
* @param maxToolIterations - Maximum tool-call loops (default: 5)
|
|
332
|
+
* @returns Promise<InferenceResult> — final LLM response with content and confidence
|
|
333
|
+
* @throws Error('Max tool iterations reached') if loop exhausted without convergence
|
|
334
|
+
* @sideEffects HTTP calls: callInference (per iteration)
|
|
335
|
+
* @sideEffects DB reads: messages (conversation history), embeddings (RAG)
|
|
336
|
+
* @calledBy runAgent
|
|
337
|
+
* @calls buildContext, callInference, dispatchTools
|
|
338
|
+
*/
|
|
339
|
+
async function executeAgentInference(
|
|
340
|
+
config: AgentConfig,
|
|
341
|
+
userMessage: string,
|
|
342
|
+
ctx: CoreContext,
|
|
343
|
+
maxToolIterations: number = 5
|
|
344
|
+
): Promise<InferenceResult> {
|
|
345
|
+
const { agent, promptConfig, thread } = config
|
|
346
|
+
|
|
347
|
+
// Build initial context
|
|
348
|
+
let context = await buildContext(config, userMessage, [], ctx)
|
|
349
|
+
|
|
350
|
+
for (let iteration = 0; iteration < maxToolIterations; iteration++) {
|
|
351
|
+
// Call inference
|
|
352
|
+
const inferenceResult = await callInference(context, agent, promptConfig, ctx)
|
|
353
|
+
|
|
354
|
+
// If no tool calls, return immediately
|
|
355
|
+
if (!inferenceResult.tool_calls || inferenceResult.tool_calls.length === 0) {
|
|
356
|
+
return inferenceResult
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Execute tools
|
|
360
|
+
const toolResults = await dispatchTools(inferenceResult.tool_calls, ctx)
|
|
361
|
+
|
|
362
|
+
// Add tool results to context and re-inference
|
|
363
|
+
context = await buildContext(config, userMessage, toolResults, ctx)
|
|
364
|
+
|
|
365
|
+
// Check if any tool failed - if so, return with error info
|
|
366
|
+
const hasErrors = toolResults.some(r => r.error)
|
|
367
|
+
if (hasErrors && iteration === maxToolIterations - 1) {
|
|
368
|
+
inferenceResult.content += '\n\n[Note: Some tools failed to execute.]'
|
|
369
|
+
return inferenceResult
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Max iterations reached
|
|
374
|
+
throw new Error('Max tool iterations reached')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Assembles the full prompt context string for a single inference call.
|
|
379
|
+
*
|
|
380
|
+
* Context sections (in order):
|
|
381
|
+
* 1. `agent.system_prompt` (or default 'You are a helpful assistant.')
|
|
382
|
+
* 2. `promptConfig.context_template` (if set)
|
|
383
|
+
* 3. Retrieved knowledge via `retrieveKnowledge` (RAG, if knowledge_sources set)
|
|
384
|
+
* 4. Conversation history via `getConversationHistory`
|
|
385
|
+
* 5. Tool results from previous iteration (if any)
|
|
386
|
+
* 6. Available tools list from `promptConfig.available_tools`
|
|
387
|
+
* 7. Current user message
|
|
388
|
+
*
|
|
389
|
+
* @param config - AgentConfig with agent, promptConfig, thread
|
|
390
|
+
* @param userMessage - Current user message
|
|
391
|
+
* @param toolResults - Results from previous tool dispatch iteration
|
|
392
|
+
* @param ctx - CoreContext
|
|
393
|
+
* @returns Promise<string> — assembled prompt context string
|
|
394
|
+
* @throws never — returns partial context on sub-call failure
|
|
395
|
+
* @sideEffects DB reads: embeddings (retrieveKnowledge), messages (history)
|
|
396
|
+
* @calledBy executeAgentInference (per iteration)
|
|
397
|
+
* @calls retrieveKnowledge, getConversationHistory
|
|
398
|
+
*/
|
|
399
|
+
async function buildContext(
|
|
400
|
+
config: AgentConfig,
|
|
401
|
+
userMessage: string,
|
|
402
|
+
toolResults: ToolResult[],
|
|
403
|
+
ctx: CoreContext
|
|
404
|
+
): Promise<string> {
|
|
405
|
+
const { agent, promptConfig, thread } = config
|
|
406
|
+
|
|
407
|
+
// 1. System prompt (from agent)
|
|
408
|
+
let context = `${agent.system_prompt || 'You are a helpful assistant.'}\n\n`
|
|
409
|
+
|
|
410
|
+
// 2. Context template (from prompt config)
|
|
411
|
+
if (promptConfig.context_template) {
|
|
412
|
+
context += `${promptConfig.context_template}\n\n`
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 3. Retrieved knowledge (RAG)
|
|
416
|
+
if (promptConfig.knowledge_sources && promptConfig.knowledge_sources.length > 0) {
|
|
417
|
+
const retrievedDocs = await retrieveKnowledge(
|
|
418
|
+
userMessage,
|
|
419
|
+
promptConfig.knowledge_sources,
|
|
420
|
+
ctx.accountId!
|
|
421
|
+
)
|
|
422
|
+
if (retrievedDocs.length > 0) {
|
|
423
|
+
context += '## Relevant Information\n'
|
|
424
|
+
retrievedDocs.forEach((doc, i) => {
|
|
425
|
+
context += `[${i + 1}] ${doc.content}\n`
|
|
426
|
+
})
|
|
427
|
+
context += '\n'
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 4. Conversation history
|
|
432
|
+
const history = await getConversationHistory(thread.id, promptConfig.max_history_messages || 20)
|
|
433
|
+
if (history.length > 0) {
|
|
434
|
+
context += '## Conversation History\n'
|
|
435
|
+
history.forEach(msg => {
|
|
436
|
+
const role = msg.data?.message_type === 'human' ? 'User' : 'Assistant'
|
|
437
|
+
context += `${role}: ${msg.content}\n`
|
|
438
|
+
})
|
|
439
|
+
context += '\n'
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 5. Tool results (if any)
|
|
443
|
+
if (toolResults.length > 0) {
|
|
444
|
+
context += '## Tool Results\n'
|
|
445
|
+
toolResults.forEach(result => {
|
|
446
|
+
if (result.error) {
|
|
447
|
+
context += `Tool "${result.tool}" failed: ${result.error}\n`
|
|
448
|
+
} else {
|
|
449
|
+
context += `Tool "${result.tool}" result: ${JSON.stringify(result.result)}\n`
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
context += '\n'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 6. Available tools (if configured)
|
|
456
|
+
if (promptConfig.available_tools && promptConfig.available_tools.length > 0) {
|
|
457
|
+
context += '## Available Tools\n'
|
|
458
|
+
promptConfig.available_tools.forEach((tool: string) => {
|
|
459
|
+
context += `- ${tool}\n`
|
|
460
|
+
})
|
|
461
|
+
context += '\n'
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 7. Current user message
|
|
465
|
+
context += `## Current Message\nUser: ${userMessage}\n\nAssistant: `
|
|
466
|
+
|
|
467
|
+
return context
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─── KNOWLEDGE & HISTORY ───────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Retrieves relevant documents from the `embeddings` table for RAG.
|
|
474
|
+
*
|
|
475
|
+
* Uses Supabase full-text search (`textSearch`) on `content`. Falls back
|
|
476
|
+
* to returning an empty array on error (never throws).
|
|
477
|
+
* Future: replace with vector similarity search when embedding service is wired.
|
|
478
|
+
*
|
|
479
|
+
* @param query - User message text to search against
|
|
480
|
+
* @param knowledgeSources - Array of document UUIDs to filter the search
|
|
481
|
+
* @param accountId - Account scope for the embeddings lookup
|
|
482
|
+
* @returns Promise<any[]> — array of embedding rows (content, metadata)
|
|
483
|
+
* @throws never — returns [] on failure
|
|
484
|
+
* @sideEffects DB read: embeddings table
|
|
485
|
+
* @calledBy buildContext (if promptConfig.knowledge_sources is non-empty)
|
|
486
|
+
*/
|
|
487
|
+
async function retrieveKnowledge(
|
|
488
|
+
query: string,
|
|
489
|
+
knowledgeSources: string[],
|
|
490
|
+
accountId: string
|
|
491
|
+
): Promise<any[]> {
|
|
492
|
+
// For now, we'll need the query embedding - this requires calling an embedding service
|
|
493
|
+
// In production, this would call the same embedding model used to create embeddings
|
|
494
|
+
// For now, we'll do a text search fallback
|
|
495
|
+
|
|
496
|
+
const { data: docs, error } = await adminDb
|
|
497
|
+
.from('embeddings')
|
|
498
|
+
.select('*')
|
|
499
|
+
.eq('account_id', accountId)
|
|
500
|
+
.in('document_id', knowledgeSources)
|
|
501
|
+
.textSearch('content', query, {
|
|
502
|
+
type: 'websearch',
|
|
503
|
+
config: 'english'
|
|
504
|
+
})
|
|
505
|
+
.limit(5)
|
|
506
|
+
|
|
507
|
+
if (error) {
|
|
508
|
+
console.error('Knowledge retrieval failed:', error)
|
|
509
|
+
return []
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return docs || []
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Loads the most recent N messages from a thread, ordered chronologically.
|
|
517
|
+
*
|
|
518
|
+
* Returns messages in ascending order (oldest first) for context assembly.
|
|
519
|
+
* Returns empty array on error (never throws).
|
|
520
|
+
*
|
|
521
|
+
* @param threadId - UUID of the thread
|
|
522
|
+
* @param limit - Maximum number of messages to return (from promptConfig.max_history_messages)
|
|
523
|
+
* @returns Promise<any[]> — array of messages rows ordered oldest-first
|
|
524
|
+
* @throws never — returns [] on failure
|
|
525
|
+
* @sideEffects DB read: messages table
|
|
526
|
+
* @calledBy buildContext
|
|
527
|
+
*/
|
|
528
|
+
async function getConversationHistory(
|
|
529
|
+
threadId: string,
|
|
530
|
+
limit: number
|
|
531
|
+
): Promise<any[]> {
|
|
532
|
+
const { data: messages, error } = await adminDb
|
|
533
|
+
.from('messages')
|
|
534
|
+
.select('*')
|
|
535
|
+
.eq('thread_id', threadId)
|
|
536
|
+
.order('created_at', { ascending: false })
|
|
537
|
+
.limit(limit)
|
|
538
|
+
|
|
539
|
+
if (error) {
|
|
540
|
+
console.error('Failed to load history:', error)
|
|
541
|
+
return []
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return messages?.reverse() || []
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ─── INFERENCE CALL ────────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Calls an OpenAI-compatible chat completions API with the assembled context.
|
|
551
|
+
*
|
|
552
|
+
* Credentials and base URL are loaded from environment variables:
|
|
553
|
+
* - `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `LLM_API_KEY`
|
|
554
|
+
* - `OPENAI_BASE_URL` / `LLM_BASE_URL` (default: 'https://api.openai.com/v1')
|
|
555
|
+
* - `LLM_DEFAULT_MODEL` (default: 'gpt-4o')
|
|
556
|
+
*
|
|
557
|
+
* If no API key is found, returns a mock response instead of throwing.
|
|
558
|
+
* This allows local development without LLM credentials.
|
|
559
|
+
*
|
|
560
|
+
* Tool calls are extracted from `message.tool_calls` and mapped to `ToolCall[]`.
|
|
561
|
+
* Confidence is derived from `logprobs` if available, otherwise defaults to 0.85.
|
|
562
|
+
*
|
|
563
|
+
* @param context - Assembled context string from buildContext
|
|
564
|
+
* @param agent - Agent record with model_config (model, temperature, max_tokens, tools)
|
|
565
|
+
* @param promptConfig - Prompt config record (unused here beyond model overrides)
|
|
566
|
+
* @param ctx - CoreContext (requestId for logging)
|
|
567
|
+
* @returns Promise<InferenceResult> — content, confidence, tool_calls, metadata
|
|
568
|
+
* @throws Error('Inference failed: <status>') on non-2xx HTTP response
|
|
569
|
+
* @sideEffects HTTP call to `${OPENAI_BASE_URL}/chat/completions`
|
|
570
|
+
* @calledBy executeAgentInference (per iteration)
|
|
571
|
+
*/
|
|
572
|
+
async function callInference(
|
|
573
|
+
context: string,
|
|
574
|
+
agent: any,
|
|
575
|
+
promptConfig: any,
|
|
576
|
+
ctx: CoreContext
|
|
577
|
+
): Promise<InferenceResult> {
|
|
578
|
+
const modelConfig = agent.model_config || {}
|
|
579
|
+
|
|
580
|
+
// Get LLM credentials from environment (safer than DB storage)
|
|
581
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.LLM_API_KEY
|
|
582
|
+
const baseUrl = process.env.OPENAI_BASE_URL || process.env.LLM_BASE_URL || 'https://api.openai.com/v1'
|
|
583
|
+
|
|
584
|
+
if (!apiKey) {
|
|
585
|
+
// Fallback: return mock response for development
|
|
586
|
+
console.warn('No LLM API key found in environment. Set OPENAI_API_KEY or LLM_API_KEY.')
|
|
587
|
+
return {
|
|
588
|
+
content: `[Mock Response] Received ${context.length} chars of context. Set OPENAI_API_KEY in .env for live inference.`,
|
|
589
|
+
confidence: 0.9,
|
|
590
|
+
tool_calls: undefined,
|
|
591
|
+
metadata: { mock: true }
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const model = modelConfig.model || process.env.LLM_DEFAULT_MODEL || 'gpt-4o'
|
|
596
|
+
const temperature = modelConfig.temperature ?? 0.7
|
|
597
|
+
const maxTokens = modelConfig.max_tokens ?? 4000
|
|
598
|
+
|
|
599
|
+
// Call OpenAI-compatible API
|
|
600
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: {
|
|
603
|
+
'Content-Type': 'application/json',
|
|
604
|
+
'Authorization': `Bearer ${apiKey}`
|
|
605
|
+
},
|
|
606
|
+
body: JSON.stringify({
|
|
607
|
+
model,
|
|
608
|
+
messages: [
|
|
609
|
+
{ role: 'system', content: context.split('\n\n')[0] }, // First paragraph as system
|
|
610
|
+
{ role: 'user', content: context.split('\n\n').slice(1).join('\n\n') } // Rest as user
|
|
611
|
+
],
|
|
612
|
+
temperature,
|
|
613
|
+
max_tokens: maxTokens,
|
|
614
|
+
...(modelConfig.tools?.length ? { tools: modelConfig.tools, tool_choice: 'auto' } : {})
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
if (!response.ok) {
|
|
619
|
+
const errorBody = await response.text()
|
|
620
|
+
throw new Error(`Inference failed: ${response.status} ${response.statusText} - ${errorBody}`)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result: any = await response.json()
|
|
624
|
+
const message = result.choices?.[0]?.message
|
|
625
|
+
|
|
626
|
+
// Extract confidence from logprobs if available, otherwise estimate
|
|
627
|
+
const confidence = result.choices?.[0]?.logprobs?.content?.[0]?.logprob
|
|
628
|
+
? Math.exp(result.choices[0].logprobs.content[0].logprob)
|
|
629
|
+
: 0.85 // Default confidence when not provided
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
content: message?.content || '',
|
|
633
|
+
confidence,
|
|
634
|
+
tool_calls: message?.tool_calls?.map((tc: any) => ({
|
|
635
|
+
id: tc.id,
|
|
636
|
+
tool: tc.function?.name,
|
|
637
|
+
params: JSON.parse(tc.function?.arguments || '{}')
|
|
638
|
+
})),
|
|
639
|
+
metadata: {
|
|
640
|
+
model: result.model,
|
|
641
|
+
usage: result.usage,
|
|
642
|
+
finish_reason: result.choices?.[0]?.finish_reason
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Legacy webhook-based inference handler. Calls an arbitrary URL with the
|
|
649
|
+
* config as JSON body. Returns a mock if `config.url` is not set.
|
|
650
|
+
*
|
|
651
|
+
* @deprecated Use `callInference` with environment-based credentials instead.
|
|
652
|
+
* @throws Error on non-2xx HTTP response
|
|
653
|
+
* @sideEffects HTTP call to config.url
|
|
654
|
+
* @calledBy unused (kept for backward compatibility)
|
|
655
|
+
*/
|
|
656
|
+
async function executeInferenceHandler(
|
|
657
|
+
config: any,
|
|
658
|
+
ctx: CoreContext
|
|
659
|
+
): Promise<any> {
|
|
660
|
+
const { url, method = 'POST', headers = {} } = config
|
|
661
|
+
|
|
662
|
+
if (!url) {
|
|
663
|
+
// Fallback: return mock response for development
|
|
664
|
+
console.warn('No inference URL configured, returning mock response')
|
|
665
|
+
return {
|
|
666
|
+
content: `[Mock] I received your message. In production, this would call ${config.model} via webhook.`,
|
|
667
|
+
confidence: 0.9
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const response = await fetch(url, {
|
|
672
|
+
method,
|
|
673
|
+
headers: {
|
|
674
|
+
'Content-Type': 'application/json',
|
|
675
|
+
...headers
|
|
676
|
+
},
|
|
677
|
+
body: JSON.stringify(config)
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
throw new Error(`Inference failed: ${response.status} ${response.statusText}`)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return await response.json()
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ─── TOOL DISPATCH ────────────────────────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Executes a batch of tool calls in sequence, returning results for each.
|
|
691
|
+
*
|
|
692
|
+
* For each tool call:
|
|
693
|
+
* 1. Look up action by `call.tool` (action.slug) in the actions table
|
|
694
|
+
* 2. Call `executeToolAction` to dispatch to the correct handler
|
|
695
|
+
* 3. Push `ToolResult` with success output or error message
|
|
696
|
+
*
|
|
697
|
+
* Individual tool failures are captured in `ToolResult.error` and do NOT
|
|
698
|
+
* halt the batch — all tool calls are attempted.
|
|
699
|
+
*
|
|
700
|
+
* @param toolCalls - Array of ToolCall objects from an InferenceResult
|
|
701
|
+
* @param ctx - CoreContext
|
|
702
|
+
* @returns Promise<ToolResult[]> — one result per input tool call
|
|
703
|
+
* @throws never — per-tool errors captured in result.error
|
|
704
|
+
* @sideEffects DB read: actions table (per tool call)
|
|
705
|
+
* @sideEffects Calls executeToolAction (DB writes, HTTP calls per handler)
|
|
706
|
+
* @calledBy executeAgentInference (after each inference call with tool_calls)
|
|
707
|
+
* @calls executeToolAction
|
|
708
|
+
*/
|
|
709
|
+
async function dispatchTools(
|
|
710
|
+
toolCalls: ToolCall[],
|
|
711
|
+
ctx: CoreContext
|
|
712
|
+
): Promise<ToolResult[]> {
|
|
713
|
+
const results: ToolResult[] = []
|
|
714
|
+
|
|
715
|
+
for (const call of toolCalls) {
|
|
716
|
+
try {
|
|
717
|
+
// Lookup action for this tool
|
|
718
|
+
const { data: action, error } = await adminDb
|
|
719
|
+
.from('actions')
|
|
720
|
+
.select('*')
|
|
721
|
+
.eq('slug', call.tool)
|
|
722
|
+
.eq('is_active', true)
|
|
723
|
+
.single()
|
|
724
|
+
|
|
725
|
+
if (error || !action) {
|
|
726
|
+
results.push({
|
|
727
|
+
tool: call.tool,
|
|
728
|
+
id: call.id,
|
|
729
|
+
result: null,
|
|
730
|
+
error: `Tool "${call.tool}" not found`
|
|
731
|
+
})
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Execute tool via pipeline-runner pattern
|
|
736
|
+
const result = await executeToolAction(action, call.params, ctx)
|
|
737
|
+
|
|
738
|
+
results.push({
|
|
739
|
+
tool: call.tool,
|
|
740
|
+
id: call.id,
|
|
741
|
+
result
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
} catch (error: any) {
|
|
745
|
+
results.push({
|
|
746
|
+
tool: call.tool,
|
|
747
|
+
id: call.id,
|
|
748
|
+
result: null,
|
|
749
|
+
error: error.message
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return results
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Dispatches a single tool action to its handler based on `action.handler`.
|
|
759
|
+
*
|
|
760
|
+
* Supported handlers (mirrors pipeline-runner stageHandlers):
|
|
761
|
+
* - `search_knowledge` → `executeSearchKnowledge`
|
|
762
|
+
* - `query_items` → `executeQueryItems`
|
|
763
|
+
* - `create_record` → `executeCreateRecord`
|
|
764
|
+
* - `update_item` → `executeUpdateItem`
|
|
765
|
+
* - `run_pipeline` → `runPipeline` (dynamic import to avoid circular deps)
|
|
766
|
+
* - `send_notification` → `executeSendNotification`
|
|
767
|
+
*
|
|
768
|
+
* @param action - Action record from the actions table
|
|
769
|
+
* @param params - Tool call parameters (from LLM function_call.arguments)
|
|
770
|
+
* @param ctx - CoreContext
|
|
771
|
+
* @returns Promise<any> — handler output
|
|
772
|
+
* @throws Error('Unknown tool handler') on unrecognised action.handler
|
|
773
|
+
* @calledBy dispatchTools
|
|
774
|
+
*/
|
|
775
|
+
async function executeToolAction(
|
|
776
|
+
action: any,
|
|
777
|
+
params: any,
|
|
778
|
+
ctx: CoreContext
|
|
779
|
+
): Promise<any> {
|
|
780
|
+
// Import pipeline-runner handlers dynamically to avoid circular deps
|
|
781
|
+
const { runPipeline } = await import('./pipeline-runner')
|
|
782
|
+
|
|
783
|
+
switch (action.handler) {
|
|
784
|
+
case 'search_knowledge':
|
|
785
|
+
return await executeSearchKnowledge(params, ctx)
|
|
786
|
+
|
|
787
|
+
case 'query_items':
|
|
788
|
+
return await executeQueryItems(params, ctx)
|
|
789
|
+
|
|
790
|
+
case 'create_record':
|
|
791
|
+
return await executeCreateRecord(params, ctx)
|
|
792
|
+
|
|
793
|
+
case 'update_item':
|
|
794
|
+
return await executeUpdateItem(params, ctx)
|
|
795
|
+
|
|
796
|
+
case 'run_pipeline':
|
|
797
|
+
const result = await runPipeline(params.pipeline_id, params.trigger_data || {}, ctx)
|
|
798
|
+
return { success: result.status === 'completed', result }
|
|
799
|
+
|
|
800
|
+
case 'send_notification':
|
|
801
|
+
return await executeSendNotification(params, ctx)
|
|
802
|
+
|
|
803
|
+
default:
|
|
804
|
+
throw new Error(`Unknown tool handler: ${action.handler}`)
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ─── TOOL HANDLERS ────────────────────────────────────────────────────────────
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Tool: search_knowledge — vector similarity search via `search_similar_embeddings`
|
|
812
|
+
* RPC, falling back to text search on error.
|
|
813
|
+
*
|
|
814
|
+
* @param params.query: string — search query text
|
|
815
|
+
* @param params.embedding: number[] | undefined — pre-computed embedding vector
|
|
816
|
+
* @param params.limit: number (default 5)
|
|
817
|
+
* @param params.threshold: number (default 0.7)
|
|
818
|
+
* @returns { results[], method: 'vector'|'text_fallback' }
|
|
819
|
+
* @throws never — falls back to text search on RPC failure
|
|
820
|
+
* @sideEffects DB: search_similar_embeddings RPC or embeddings text search
|
|
821
|
+
*/
|
|
822
|
+
async function executeSearchKnowledge(
|
|
823
|
+
params: any,
|
|
824
|
+
ctx: CoreContext
|
|
825
|
+
): Promise<any> {
|
|
826
|
+
const { query, knowledge_sources, limit = 5 } = params
|
|
827
|
+
|
|
828
|
+
const { data: results, error } = await adminDb.rpc('search_similar_embeddings', {
|
|
829
|
+
p_account_id: ctx.accountId,
|
|
830
|
+
p_model_id: params.model_id || 'text-embedding-ada-002',
|
|
831
|
+
p_query_embedding: params.embedding, // If pre-computed
|
|
832
|
+
p_limit: limit,
|
|
833
|
+
p_threshold: params.threshold || 0.7
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
if (error) {
|
|
837
|
+
// Fallback to text search
|
|
838
|
+
const { data: fallback } = await adminDb
|
|
839
|
+
.from('embeddings')
|
|
840
|
+
.select('*')
|
|
841
|
+
.eq('account_id', ctx.accountId)
|
|
842
|
+
.textSearch('content', query)
|
|
843
|
+
.limit(limit)
|
|
844
|
+
|
|
845
|
+
return { results: fallback || [], method: 'text_fallback' }
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return { results: results || [], method: 'vector' }
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Tool: query_items — filtered SELECT on any table scoped to ctx.accountId.
|
|
853
|
+
*
|
|
854
|
+
* @param params.entity: string — table name
|
|
855
|
+
* @param params.filters: Record<string, any> (default {})
|
|
856
|
+
* @param params.limit: number (default 10)
|
|
857
|
+
* @returns { items[] }
|
|
858
|
+
* @throws Error on DB failure
|
|
859
|
+
* @sideEffects DB read: params.entity table
|
|
860
|
+
*/
|
|
861
|
+
async function executeQueryItems(
|
|
862
|
+
params: any,
|
|
863
|
+
ctx: CoreContext
|
|
864
|
+
): Promise<any> {
|
|
865
|
+
const { entity, filters = {}, limit = 10 } = params
|
|
866
|
+
|
|
867
|
+
let query = adminDb
|
|
868
|
+
.from(entity)
|
|
869
|
+
.select('*')
|
|
870
|
+
.eq('account_id', ctx.accountId)
|
|
871
|
+
.limit(limit)
|
|
872
|
+
|
|
873
|
+
// Apply filters
|
|
874
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
875
|
+
query = query.eq(key, value)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
const { data, error } = await query
|
|
879
|
+
|
|
880
|
+
if (error) throw new Error(`Query failed: ${error.message}`)
|
|
881
|
+
return { items: data || [] }
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Tool: create_record — inserts a record into any table.
|
|
886
|
+
*
|
|
887
|
+
* @param params.entity: string — table name
|
|
888
|
+
* @param params.data: Record<string, any> — field values
|
|
889
|
+
* @returns { record } — the inserted row
|
|
890
|
+
* @throws Error on DB failure
|
|
891
|
+
* @sideEffects DB write: params.entity table
|
|
892
|
+
*/
|
|
893
|
+
async function executeCreateRecord(
|
|
894
|
+
params: any,
|
|
895
|
+
ctx: CoreContext
|
|
896
|
+
): Promise<any> {
|
|
897
|
+
const { entity, data } = params
|
|
898
|
+
|
|
899
|
+
const { data: result, error } = await adminDb
|
|
900
|
+
.from(entity)
|
|
901
|
+
.insert({
|
|
902
|
+
...data,
|
|
903
|
+
account_id: ctx.accountId,
|
|
904
|
+
created_by: ctx.principal?.id,
|
|
905
|
+
created_at: new Date().toISOString(),
|
|
906
|
+
updated_at: new Date().toISOString()
|
|
907
|
+
})
|
|
908
|
+
.select()
|
|
909
|
+
.single()
|
|
910
|
+
|
|
911
|
+
if (error) throw new Error(`Create failed: ${error.message}`)
|
|
912
|
+
return { record: result }
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Tool: update_item — updates a record by ID in any table.
|
|
917
|
+
*
|
|
918
|
+
* @param params.entity: string — table name
|
|
919
|
+
* @param params.record_id: string — UUID of the record
|
|
920
|
+
* @param params.data: Record<string, any> — fields to update
|
|
921
|
+
* @returns { record } — the updated row
|
|
922
|
+
* @throws Error on DB failure
|
|
923
|
+
* @sideEffects DB write: params.entity table
|
|
924
|
+
*/
|
|
925
|
+
async function executeUpdateItem(
|
|
926
|
+
params: any,
|
|
927
|
+
ctx: CoreContext
|
|
928
|
+
): Promise<any> {
|
|
929
|
+
const { entity, record_id, data } = params
|
|
930
|
+
|
|
931
|
+
const { data: result, error } = await adminDb
|
|
932
|
+
.from(entity)
|
|
933
|
+
.update({
|
|
934
|
+
...data,
|
|
935
|
+
updated_at: new Date().toISOString(),
|
|
936
|
+
updated_by: ctx.principal?.id
|
|
937
|
+
})
|
|
938
|
+
.eq('id', record_id)
|
|
939
|
+
.select()
|
|
940
|
+
.single()
|
|
941
|
+
|
|
942
|
+
if (error) throw new Error(`Update failed: ${error.message}`)
|
|
943
|
+
return { record: result }
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Tool: send_notification — inserts rows into the `watchers` table.
|
|
948
|
+
*
|
|
949
|
+
* @param params.message: string — notification text
|
|
950
|
+
* @param params.recipients: string[] — person UUIDs to notify
|
|
951
|
+
* @param params.entity_type: string | undefined
|
|
952
|
+
* @param params.entity_id: string | undefined
|
|
953
|
+
* @returns { notified_count }
|
|
954
|
+
* @throws Error on DB failure
|
|
955
|
+
* @sideEffects DB write: watchers table
|
|
956
|
+
*/
|
|
957
|
+
async function executeSendNotification(
|
|
958
|
+
params: any,
|
|
959
|
+
ctx: CoreContext
|
|
960
|
+
): Promise<any> {
|
|
961
|
+
const { message, recipients = [], entity_type, entity_id } = params
|
|
962
|
+
|
|
963
|
+
const notifications = recipients.map((recipientId: string) => ({
|
|
964
|
+
account_id: ctx.accountId,
|
|
965
|
+
person_id: recipientId,
|
|
966
|
+
message,
|
|
967
|
+
entity_type: entity_type || 'agent_message',
|
|
968
|
+
entity_id: entity_id || ctx.requestId,
|
|
969
|
+
is_read: false,
|
|
970
|
+
created_at: new Date().toISOString()
|
|
971
|
+
}))
|
|
972
|
+
|
|
973
|
+
if (notifications.length > 0) {
|
|
974
|
+
const { error } = await adminDb.from('watchers').insert(notifications)
|
|
975
|
+
if (error) throw new Error(`Notification failed: ${error.message}`)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return { notified_count: notifications.length }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ─── ESCALATION ────────────────────────────────────────────────────────────
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Handles confidence-based escalation when inference confidence falls below
|
|
985
|
+
* `promptConfig.confidence_threshold`.
|
|
986
|
+
*
|
|
987
|
+
* Actions taken:
|
|
988
|
+
* 1. Attempt to set `threads.data.escalation_status = 'pending'` (via jsonb_set RPC)
|
|
989
|
+
* 2. Emit `agent.inference.low_confidence` audit log
|
|
990
|
+
* 3. If `promptConfig.escalation_action === 'pipeline'` and
|
|
991
|
+
* `promptConfig.escalation_target` is set, run the escalation pipeline
|
|
992
|
+
* via dynamic import of `runPipeline`
|
|
993
|
+
*
|
|
994
|
+
* @param threadId - UUID of the thread being escalated
|
|
995
|
+
* @param confidence - The confidence score that triggered escalation
|
|
996
|
+
* @param promptConfig - Prompt config with escalation settings
|
|
997
|
+
* @param ctx - CoreContext
|
|
998
|
+
* @returns Promise<void> — always resolves
|
|
999
|
+
* @throws never — DB errors are silently dropped (best-effort)
|
|
1000
|
+
* @sideEffects DB write: threads.data (jsonb_set for escalation_status)
|
|
1001
|
+
* @sideEffects DB write: emitAudit (agent.inference.low_confidence)
|
|
1002
|
+
* @sideEffects Calls runPipeline (if escalation_action === 'pipeline')
|
|
1003
|
+
* @calledBy runAgent (when agentResponse.confidence < threshold)
|
|
1004
|
+
*/
|
|
1005
|
+
async function handleEscalation(
|
|
1006
|
+
threadId: string,
|
|
1007
|
+
confidence: number,
|
|
1008
|
+
promptConfig: any,
|
|
1009
|
+
ctx: CoreContext
|
|
1010
|
+
): Promise<void> {
|
|
1011
|
+
// Update thread escalation status
|
|
1012
|
+
await adminDb
|
|
1013
|
+
.from('threads')
|
|
1014
|
+
.update({
|
|
1015
|
+
data: adminDb.rpc('jsonb_set', {
|
|
1016
|
+
target: adminDb.from('threads').select('data').eq('id', threadId).single(),
|
|
1017
|
+
path: '{escalation_status}',
|
|
1018
|
+
new_value: '"pending"'
|
|
1019
|
+
})
|
|
1020
|
+
})
|
|
1021
|
+
.eq('id', threadId)
|
|
1022
|
+
|
|
1023
|
+
// Fire trigger event
|
|
1024
|
+
await emitAudit(ctx, 'agent.inference.low_confidence', {
|
|
1025
|
+
type: 'thread',
|
|
1026
|
+
id: threadId,
|
|
1027
|
+
account_id: ctx.accountId ?? undefined
|
|
1028
|
+
}, {
|
|
1029
|
+
confidence,
|
|
1030
|
+
threshold: promptConfig.confidence_threshold,
|
|
1031
|
+
escalation_action: promptConfig.escalation_action,
|
|
1032
|
+
escalation_target: promptConfig.escalation_target
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
// If escalation pipeline configured, run it
|
|
1036
|
+
if (promptConfig.escalation_action === 'pipeline' && promptConfig.escalation_target) {
|
|
1037
|
+
const { runPipeline } = await import('./pipeline-runner')
|
|
1038
|
+
await runPipeline(
|
|
1039
|
+
promptConfig.escalation_target,
|
|
1040
|
+
{
|
|
1041
|
+
thread_id: threadId,
|
|
1042
|
+
confidence,
|
|
1043
|
+
threshold: promptConfig.confidence_threshold,
|
|
1044
|
+
reason: 'low_confidence'
|
|
1045
|
+
},
|
|
1046
|
+
ctx
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// ─── MESSAGE PERSISTENCE ───────────────────────────────────────────────────────────
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Inserts a message row into the `messages` table for a thread.
|
|
1055
|
+
*
|
|
1056
|
+
* For human messages: `person_id` is set to `ctx.principal.id`.
|
|
1057
|
+
* For agent/system/tool messages: `person_id` is null.
|
|
1058
|
+
* The `data` JSONB field carries `message_type` and any additional metadata.
|
|
1059
|
+
*
|
|
1060
|
+
* @param threadId - UUID of the parent thread
|
|
1061
|
+
* @param content - Message text content
|
|
1062
|
+
* @param messageType - Role classifier for the message
|
|
1063
|
+
* @param data - Additional JSONB metadata (e.g. confidence, tool_calls, agent_id)
|
|
1064
|
+
* @param ctx - CoreContext with accountId and principal
|
|
1065
|
+
* @returns Promise<any> — the inserted messages row
|
|
1066
|
+
* @throws Error('Failed to save message') on DB insert failure
|
|
1067
|
+
* @inputSpec messageType: 'human'|'agent'|'system'|'tool_call'|'tool_result'
|
|
1068
|
+
* @sideEffects DB write: messages table
|
|
1069
|
+
* @calledBy runAgent (user message + agent response)
|
|
1070
|
+
*/
|
|
1071
|
+
async function saveMessage(
|
|
1072
|
+
threadId: string,
|
|
1073
|
+
content: string,
|
|
1074
|
+
messageType: 'human' | 'agent' | 'system' | 'tool_call' | 'tool_result',
|
|
1075
|
+
data: any,
|
|
1076
|
+
ctx: CoreContext
|
|
1077
|
+
): Promise<any> {
|
|
1078
|
+
const { data: message, error } = await adminDb
|
|
1079
|
+
.from('messages')
|
|
1080
|
+
.insert({
|
|
1081
|
+
thread_id: threadId,
|
|
1082
|
+
account_id: ctx.accountId,
|
|
1083
|
+
person_id: messageType === 'human' ? ctx.principal?.id : null,
|
|
1084
|
+
content,
|
|
1085
|
+
content_format: 'text',
|
|
1086
|
+
data: {
|
|
1087
|
+
message_type: messageType,
|
|
1088
|
+
...data
|
|
1089
|
+
},
|
|
1090
|
+
created_at: new Date().toISOString()
|
|
1091
|
+
})
|
|
1092
|
+
.select()
|
|
1093
|
+
.single()
|
|
1094
|
+
|
|
1095
|
+
if (error) throw new Error(`Failed to save message: ${error.message}`)
|
|
1096
|
+
return message
|
|
1097
|
+
}
|