@vellumai/assistant 0.5.1 → 0.5.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/ARCHITECTURE.md +163 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/docs/skills.md +100 -0
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
- package/src/__tests__/conversation-agent-loop.test.ts +297 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-wipe.test.ts +226 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +7 -7
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +699 -0
- package/src/__tests__/memory-reducer.test.ts +698 -0
- package/src/__tests__/memory-regressions.test.ts +6 -4
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skill-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +24 -0
- package/src/config/loader.ts +65 -0
- package/src/config/raw-config-utils.ts +58 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +20 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/services.ts +8 -6
- package/src/config/skills.ts +50 -4
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
- package/src/daemon/conversation-agent-loop.ts +127 -20
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +50 -16
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +22 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +170 -24
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +69 -30
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +156 -0
- package/src/daemon/handlers/config-model.ts +62 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +115 -65
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +18 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/followups/followup-store.ts +47 -1
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/archive-store.ts +400 -0
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +161 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +591 -8
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +40 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +114 -21
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +2 -4
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +6 -0
- package/src/memory/jobs-worker.ts +12 -0
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +99 -0
- package/src/memory/reducer.ts +453 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/schema/oauth.ts +6 -0
- package/src/memory/search/semantic.ts +17 -4
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +99 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +160 -14
- package/src/permissions/defaults.ts +14 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +74 -0
- package/src/runtime/routes/conversation-query-routes.ts +187 -10
- package/src/runtime/routes/conversation-routes.ts +269 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1212 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +94 -5
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +30 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +30 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/tasks/task-store.ts +43 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/load.ts +140 -6
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +24 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +11 -1
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured result types for the simplified memory reducer.
|
|
3
|
+
*
|
|
4
|
+
* The reducer processes conversation turns and produces CRUD operations for
|
|
5
|
+
* two brief-state tables (time_contexts, open_loops) and optional archive
|
|
6
|
+
* candidates (observations, episodes).
|
|
7
|
+
*
|
|
8
|
+
* These types are consumed by the reducer parser/validator and eventually by
|
|
9
|
+
* the DB-write layer that applies them atomically.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Time-context CRUD ──────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface TimeContextCreate {
|
|
15
|
+
action: "create";
|
|
16
|
+
summary: string;
|
|
17
|
+
source: string;
|
|
18
|
+
activeFrom: number; // epoch ms
|
|
19
|
+
activeUntil: number; // epoch ms
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TimeContextUpdate {
|
|
23
|
+
action: "update";
|
|
24
|
+
id: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
activeFrom?: number;
|
|
27
|
+
activeUntil?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TimeContextResolve {
|
|
31
|
+
action: "resolve";
|
|
32
|
+
id: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type TimeContextOp =
|
|
36
|
+
| TimeContextCreate
|
|
37
|
+
| TimeContextUpdate
|
|
38
|
+
| TimeContextResolve;
|
|
39
|
+
|
|
40
|
+
// ── Open-loop CRUD ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface OpenLoopCreate {
|
|
43
|
+
action: "create";
|
|
44
|
+
summary: string;
|
|
45
|
+
source: string;
|
|
46
|
+
dueAt?: number; // epoch ms, optional deadline
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface OpenLoopUpdate {
|
|
50
|
+
action: "update";
|
|
51
|
+
id: string;
|
|
52
|
+
summary?: string;
|
|
53
|
+
dueAt?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OpenLoopResolve {
|
|
57
|
+
action: "resolve";
|
|
58
|
+
id: string;
|
|
59
|
+
status: "resolved" | "expired";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type OpenLoopOp = OpenLoopCreate | OpenLoopUpdate | OpenLoopResolve;
|
|
63
|
+
|
|
64
|
+
// ── Archive candidates ─────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export interface ArchiveObservationCandidate {
|
|
67
|
+
content: string;
|
|
68
|
+
role: string;
|
|
69
|
+
modality?: string;
|
|
70
|
+
source?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ArchiveEpisodeCandidate {
|
|
74
|
+
title: string;
|
|
75
|
+
summary: string;
|
|
76
|
+
source?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Top-level reducer result ───────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export interface ReducerResult {
|
|
82
|
+
timeContexts: TimeContextOp[];
|
|
83
|
+
openLoops: OpenLoopOp[];
|
|
84
|
+
archiveObservations: ArchiveObservationCandidate[];
|
|
85
|
+
archiveEpisodes: ArchiveEpisodeCandidate[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* An empty result used as fallback when the reducer output is invalid or
|
|
90
|
+
* unparseable. Guarantees no side-effects on the DB.
|
|
91
|
+
*/
|
|
92
|
+
export const EMPTY_REDUCER_RESULT: Readonly<ReducerResult> = Object.freeze({
|
|
93
|
+
timeContexts: Object.freeze([]) as unknown as TimeContextOp[],
|
|
94
|
+
openLoops: Object.freeze([]) as unknown as OpenLoopOp[],
|
|
95
|
+
archiveObservations: Object.freeze(
|
|
96
|
+
[],
|
|
97
|
+
) as unknown as ArchiveObservationCandidate[],
|
|
98
|
+
archiveEpisodes: Object.freeze([]) as unknown as ArchiveEpisodeCandidate[],
|
|
99
|
+
});
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified memory reducer — provider-backed conversation turn processor.
|
|
3
|
+
*
|
|
4
|
+
* This module owns:
|
|
5
|
+
* 1. ReducerPromptInput — structured input for the provider call
|
|
6
|
+
* 2. runReducer — send the transcript span to the LLM and return a typed result
|
|
7
|
+
* 3. parseReducerOutput — raw string -> validated ReducerResult
|
|
8
|
+
* 4. Fallback to EMPTY_REDUCER_RESULT on any invalid output
|
|
9
|
+
*
|
|
10
|
+
* The reducer is intentionally side-effect-free: it never writes to the
|
|
11
|
+
* database. Callers are responsible for applying the returned ReducerResult.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createTimeout,
|
|
16
|
+
extractText,
|
|
17
|
+
getConfiguredProvider,
|
|
18
|
+
} from "../providers/provider-send-message.js";
|
|
19
|
+
import { getLogger } from "../util/logger.js";
|
|
20
|
+
import {
|
|
21
|
+
type ArchiveEpisodeCandidate,
|
|
22
|
+
type ArchiveObservationCandidate,
|
|
23
|
+
EMPTY_REDUCER_RESULT,
|
|
24
|
+
type OpenLoopCreate,
|
|
25
|
+
type OpenLoopOp,
|
|
26
|
+
type OpenLoopUpdate,
|
|
27
|
+
type ReducerResult,
|
|
28
|
+
type TimeContextOp,
|
|
29
|
+
type TimeContextUpdate,
|
|
30
|
+
} from "./reducer-types.js";
|
|
31
|
+
|
|
32
|
+
const log = getLogger("memory-reducer");
|
|
33
|
+
|
|
34
|
+
/** Timeout for the reducer provider call (ms). */
|
|
35
|
+
const REDUCER_TIMEOUT_MS = 30_000;
|
|
36
|
+
|
|
37
|
+
// ── Prompt input type ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** The structured input that will be fed to the reducer provider call. */
|
|
40
|
+
export interface ReducerPromptInput {
|
|
41
|
+
/** Conversation ID being reduced. */
|
|
42
|
+
conversationId: string;
|
|
43
|
+
/** New messages since the last reduction checkpoint (role + content). */
|
|
44
|
+
newMessages: Array<{ role: string; content: string }>;
|
|
45
|
+
/** Current time-context rows the model can reference for updates. */
|
|
46
|
+
existingTimeContexts: Array<{ id: string; summary: string }>;
|
|
47
|
+
/** Current open-loop rows the model can reference for updates. */
|
|
48
|
+
existingOpenLoops: Array<{ id: string; summary: string; status: string }>;
|
|
49
|
+
/** Current time as epoch ms — injected for deterministic tests. */
|
|
50
|
+
nowMs: number;
|
|
51
|
+
/** Memory scope identifier (e.g. assistant instance ID). */
|
|
52
|
+
scopeId: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── System prompt ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the reducer system prompt. Extracted as a named function so tests can
|
|
59
|
+
* assert on prompt content without coupling to string literals.
|
|
60
|
+
*/
|
|
61
|
+
export function buildReducerSystemPrompt(): string {
|
|
62
|
+
return [
|
|
63
|
+
"You are a memory reducer for a personal assistant. Your job is to analyze",
|
|
64
|
+
"a span of new conversation messages and produce structured JSON output that",
|
|
65
|
+
"captures important information for the assistant's long-term memory.",
|
|
66
|
+
"",
|
|
67
|
+
"You output a single JSON object with four optional arrays:",
|
|
68
|
+
"",
|
|
69
|
+
"1. `timeContexts` — time-bounded situational context (e.g. 'user traveling next week').",
|
|
70
|
+
" Each entry has: action ('create'|'update'|'resolve'), and fields depending on the action.",
|
|
71
|
+
" - create: summary (string), source (string), activeFrom (epoch ms), activeUntil (epoch ms)",
|
|
72
|
+
" - update: id (string), and at least one of: summary, activeFrom, activeUntil",
|
|
73
|
+
" - resolve: id (string)",
|
|
74
|
+
"",
|
|
75
|
+
"2. `openLoops` — unresolved items to track (e.g. 'waiting for Bob's reply').",
|
|
76
|
+
" Each entry has: action ('create'|'update'|'resolve'), and fields depending on the action.",
|
|
77
|
+
" - create: summary (string), source (string), optional dueAt (epoch ms)",
|
|
78
|
+
" - update: id (string), and at least one of: summary, dueAt",
|
|
79
|
+
" - resolve: id (string), status ('resolved'|'expired')",
|
|
80
|
+
"",
|
|
81
|
+
"3. `archiveObservations` — factual statements extracted from the conversation.",
|
|
82
|
+
" Each entry has: content (string), role (string), optional modality (string), optional source (string)",
|
|
83
|
+
"",
|
|
84
|
+
"4. `archiveEpisodes` — coherent narrative summaries of interaction spans.",
|
|
85
|
+
" Each entry has: title (string), summary (string), optional source (string)",
|
|
86
|
+
"",
|
|
87
|
+
"Rules:",
|
|
88
|
+
"- Output ONLY valid JSON. No markdown, no explanation, no wrapping.",
|
|
89
|
+
"- Omit arrays that would be empty rather than including empty arrays.",
|
|
90
|
+
"- For updates and resolves, reference existing IDs from the provided context.",
|
|
91
|
+
"- Be selective: only extract genuinely important or actionable information.",
|
|
92
|
+
"- Timestamps are in epoch milliseconds.",
|
|
93
|
+
"- If there is nothing meaningful to extract, output: {}",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the user-message content for the reducer prompt from the structured input.
|
|
99
|
+
*/
|
|
100
|
+
export function buildReducerUserMessage(input: ReducerPromptInput): string {
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
|
|
103
|
+
parts.push(
|
|
104
|
+
`Current time: ${new Date(input.nowMs).toISOString()} (${input.nowMs}ms)`,
|
|
105
|
+
);
|
|
106
|
+
parts.push(`Conversation: ${input.conversationId}`);
|
|
107
|
+
parts.push(`Scope: ${input.scopeId}`);
|
|
108
|
+
parts.push("");
|
|
109
|
+
|
|
110
|
+
// Existing state the model can reference for updates/resolves
|
|
111
|
+
if (input.existingTimeContexts.length > 0) {
|
|
112
|
+
parts.push("## Active time contexts");
|
|
113
|
+
for (const tc of input.existingTimeContexts) {
|
|
114
|
+
parts.push(`- [${tc.id}] ${tc.summary}`);
|
|
115
|
+
}
|
|
116
|
+
parts.push("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (input.existingOpenLoops.length > 0) {
|
|
120
|
+
parts.push("## Active open loops");
|
|
121
|
+
for (const ol of input.existingOpenLoops) {
|
|
122
|
+
parts.push(`- [${ol.id}] (${ol.status}) ${ol.summary}`);
|
|
123
|
+
}
|
|
124
|
+
parts.push("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The unreduced transcript span
|
|
128
|
+
parts.push("## New messages to process");
|
|
129
|
+
for (const msg of input.newMessages) {
|
|
130
|
+
parts.push(`[${msg.role}]: ${msg.content}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return parts.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Provider-backed reducer call ──────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run the memory reducer against a transcript span.
|
|
140
|
+
*
|
|
141
|
+
* Sends the unreduced messages, active time contexts, active open loops,
|
|
142
|
+
* current time, and scope metadata to the configured LLM provider. Parses
|
|
143
|
+
* the response into a typed {@link ReducerResult}.
|
|
144
|
+
*
|
|
145
|
+
* This function is **side-effect-free**: it never writes to the database.
|
|
146
|
+
* The caller is responsible for applying the returned result.
|
|
147
|
+
*
|
|
148
|
+
* Returns {@link EMPTY_REDUCER_RESULT} when:
|
|
149
|
+
* - No provider is configured/available
|
|
150
|
+
* - The provider call fails or times out
|
|
151
|
+
* - The model output is unparseable
|
|
152
|
+
*
|
|
153
|
+
* @param input Structured reducer input
|
|
154
|
+
* @param signal Optional external abort signal
|
|
155
|
+
*/
|
|
156
|
+
export async function runReducer(
|
|
157
|
+
input: ReducerPromptInput,
|
|
158
|
+
signal?: AbortSignal,
|
|
159
|
+
): Promise<ReducerResult> {
|
|
160
|
+
const provider = await getConfiguredProvider();
|
|
161
|
+
if (!provider) {
|
|
162
|
+
log.warn(
|
|
163
|
+
"No provider available for memory reducer — returning empty result",
|
|
164
|
+
);
|
|
165
|
+
return EMPTY_REDUCER_RESULT;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const systemPrompt = buildReducerSystemPrompt();
|
|
169
|
+
const userText = buildReducerUserMessage(input);
|
|
170
|
+
|
|
171
|
+
const { signal: timeoutSignal, cleanup } = createTimeout(REDUCER_TIMEOUT_MS);
|
|
172
|
+
const combinedSignal = signal
|
|
173
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
174
|
+
: timeoutSignal;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const response = await provider.sendMessage(
|
|
178
|
+
[{ role: "user", content: [{ type: "text", text: userText }] }],
|
|
179
|
+
undefined,
|
|
180
|
+
systemPrompt,
|
|
181
|
+
{
|
|
182
|
+
signal: combinedSignal,
|
|
183
|
+
config: {
|
|
184
|
+
modelIntent: "latency-optimized" as const,
|
|
185
|
+
max_tokens: 4096,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const rawText = extractText(response);
|
|
191
|
+
if (!rawText) {
|
|
192
|
+
log.warn("Reducer provider returned empty text — returning empty result");
|
|
193
|
+
return EMPTY_REDUCER_RESULT;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return parseReducerOutput(rawText);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (combinedSignal.aborted) {
|
|
199
|
+
log.warn("Memory reducer provider call timed out or was aborted");
|
|
200
|
+
} else {
|
|
201
|
+
log.warn({ err }, "Memory reducer provider call failed");
|
|
202
|
+
}
|
|
203
|
+
return EMPTY_REDUCER_RESULT;
|
|
204
|
+
} finally {
|
|
205
|
+
cleanup();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Validation helpers ─────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const VALID_TIME_CONTEXT_ACTIONS = new Set(["create", "update", "resolve"]);
|
|
212
|
+
const VALID_OPEN_LOOP_ACTIONS = new Set(["create", "update", "resolve"]);
|
|
213
|
+
const VALID_OPEN_LOOP_RESOLVE_STATUSES = new Set(["resolved", "expired"]);
|
|
214
|
+
|
|
215
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
216
|
+
return typeof v === "string" && v.length > 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isPositiveNumber(v: unknown): v is number {
|
|
220
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isNonNegativeNumber(v: unknown): v is number {
|
|
224
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateTimeContextOp(raw: unknown): TimeContextOp | null {
|
|
228
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
229
|
+
const obj = raw as Record<string, unknown>;
|
|
230
|
+
const action = obj.action;
|
|
231
|
+
|
|
232
|
+
if (!isNonEmptyString(action) || !VALID_TIME_CONTEXT_ACTIONS.has(action)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (action === "create") {
|
|
237
|
+
if (
|
|
238
|
+
!isNonEmptyString(obj.summary) ||
|
|
239
|
+
!isNonEmptyString(obj.source) ||
|
|
240
|
+
!isNonNegativeNumber(obj.activeFrom) ||
|
|
241
|
+
!isPositiveNumber(obj.activeUntil)
|
|
242
|
+
) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
action: "create",
|
|
247
|
+
summary: obj.summary,
|
|
248
|
+
source: obj.source,
|
|
249
|
+
activeFrom: obj.activeFrom,
|
|
250
|
+
activeUntil: obj.activeUntil,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (action === "update") {
|
|
255
|
+
if (!isNonEmptyString(obj.id)) return null;
|
|
256
|
+
// Extract and narrow optional fields
|
|
257
|
+
const summary = isNonEmptyString(obj.summary) ? obj.summary : undefined;
|
|
258
|
+
const activeFrom = isNonNegativeNumber(obj.activeFrom)
|
|
259
|
+
? obj.activeFrom
|
|
260
|
+
: undefined;
|
|
261
|
+
const activeUntil = isPositiveNumber(obj.activeUntil)
|
|
262
|
+
? obj.activeUntil
|
|
263
|
+
: undefined;
|
|
264
|
+
// At least one field must be provided for the update to be meaningful
|
|
265
|
+
if (
|
|
266
|
+
summary === undefined &&
|
|
267
|
+
activeFrom === undefined &&
|
|
268
|
+
activeUntil === undefined
|
|
269
|
+
) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const result: TimeContextUpdate = {
|
|
273
|
+
action: "update",
|
|
274
|
+
id: obj.id,
|
|
275
|
+
};
|
|
276
|
+
if (summary !== undefined) result.summary = summary;
|
|
277
|
+
if (activeFrom !== undefined) result.activeFrom = activeFrom;
|
|
278
|
+
if (activeUntil !== undefined) result.activeUntil = activeUntil;
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// resolve
|
|
283
|
+
if (!isNonEmptyString(obj.id)) return null;
|
|
284
|
+
return { action: "resolve", id: obj.id };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function validateOpenLoopOp(raw: unknown): OpenLoopOp | null {
|
|
288
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
289
|
+
const obj = raw as Record<string, unknown>;
|
|
290
|
+
const action = obj.action;
|
|
291
|
+
|
|
292
|
+
if (!isNonEmptyString(action) || !VALID_OPEN_LOOP_ACTIONS.has(action)) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (action === "create") {
|
|
297
|
+
if (!isNonEmptyString(obj.summary) || !isNonEmptyString(obj.source)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const result: OpenLoopCreate = {
|
|
301
|
+
action: "create",
|
|
302
|
+
summary: obj.summary,
|
|
303
|
+
source: obj.source,
|
|
304
|
+
};
|
|
305
|
+
const dueAt = isNonNegativeNumber(obj.dueAt) ? obj.dueAt : undefined;
|
|
306
|
+
if (dueAt !== undefined) result.dueAt = dueAt;
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (action === "update") {
|
|
311
|
+
if (!isNonEmptyString(obj.id)) return null;
|
|
312
|
+
const summary = isNonEmptyString(obj.summary) ? obj.summary : undefined;
|
|
313
|
+
const dueAt = isNonNegativeNumber(obj.dueAt) ? obj.dueAt : undefined;
|
|
314
|
+
if (summary === undefined && dueAt === undefined) return null;
|
|
315
|
+
|
|
316
|
+
const result: OpenLoopUpdate = {
|
|
317
|
+
action: "update",
|
|
318
|
+
id: obj.id,
|
|
319
|
+
};
|
|
320
|
+
if (summary !== undefined) result.summary = summary;
|
|
321
|
+
if (dueAt !== undefined) result.dueAt = dueAt;
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// resolve
|
|
326
|
+
if (!isNonEmptyString(obj.id)) return null;
|
|
327
|
+
if (
|
|
328
|
+
!isNonEmptyString(obj.status) ||
|
|
329
|
+
!VALID_OPEN_LOOP_RESOLVE_STATUSES.has(obj.status)
|
|
330
|
+
) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
action: "resolve",
|
|
335
|
+
id: obj.id,
|
|
336
|
+
status: obj.status as "resolved" | "expired",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function validateArchiveObservation(
|
|
341
|
+
raw: unknown,
|
|
342
|
+
): ArchiveObservationCandidate | null {
|
|
343
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
344
|
+
const obj = raw as Record<string, unknown>;
|
|
345
|
+
if (!isNonEmptyString(obj.content) || !isNonEmptyString(obj.role)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const result: ArchiveObservationCandidate = {
|
|
349
|
+
content: obj.content,
|
|
350
|
+
role: obj.role,
|
|
351
|
+
};
|
|
352
|
+
if (isNonEmptyString(obj.modality)) result.modality = obj.modality;
|
|
353
|
+
if (isNonEmptyString(obj.source)) result.source = obj.source;
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validateArchiveEpisode(raw: unknown): ArchiveEpisodeCandidate | null {
|
|
358
|
+
if (raw == null || typeof raw !== "object") return null;
|
|
359
|
+
const obj = raw as Record<string, unknown>;
|
|
360
|
+
if (!isNonEmptyString(obj.title) || !isNonEmptyString(obj.summary)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const result: ArchiveEpisodeCandidate = {
|
|
364
|
+
title: obj.title,
|
|
365
|
+
summary: obj.summary,
|
|
366
|
+
};
|
|
367
|
+
if (isNonEmptyString(obj.source)) result.source = obj.source;
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Parse raw model output into a validated ReducerResult.
|
|
375
|
+
*
|
|
376
|
+
* On any structural error (non-JSON, missing top-level keys, wrong types)
|
|
377
|
+
* the function returns EMPTY_REDUCER_RESULT rather than throwing. Individual
|
|
378
|
+
* invalid operations within an otherwise valid structure are silently dropped
|
|
379
|
+
* to preserve the rest of the result.
|
|
380
|
+
*
|
|
381
|
+
* However, if **all four** top-level arrays are absent or not arrays, the
|
|
382
|
+
* entire output is treated as invalid and returns the empty result.
|
|
383
|
+
*/
|
|
384
|
+
export function parseReducerOutput(raw: string): ReducerResult {
|
|
385
|
+
let parsed: unknown;
|
|
386
|
+
try {
|
|
387
|
+
parsed = JSON.parse(raw);
|
|
388
|
+
} catch {
|
|
389
|
+
log.warn("reducer output is not valid JSON — falling back to empty result");
|
|
390
|
+
return EMPTY_REDUCER_RESULT;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
394
|
+
log.warn(
|
|
395
|
+
"reducer output is not a JSON object — falling back to empty result",
|
|
396
|
+
);
|
|
397
|
+
return EMPTY_REDUCER_RESULT;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const obj = parsed as Record<string, unknown>;
|
|
401
|
+
|
|
402
|
+
// Check that at least one top-level array key exists
|
|
403
|
+
const hasTimeContexts = Array.isArray(obj.timeContexts);
|
|
404
|
+
const hasOpenLoops = Array.isArray(obj.openLoops);
|
|
405
|
+
const hasArchiveObservations = Array.isArray(obj.archiveObservations);
|
|
406
|
+
const hasArchiveEpisodes = Array.isArray(obj.archiveEpisodes);
|
|
407
|
+
|
|
408
|
+
if (
|
|
409
|
+
!hasTimeContexts &&
|
|
410
|
+
!hasOpenLoops &&
|
|
411
|
+
!hasArchiveObservations &&
|
|
412
|
+
!hasArchiveEpisodes
|
|
413
|
+
) {
|
|
414
|
+
log.warn(
|
|
415
|
+
"reducer output has no recognized top-level arrays — falling back to empty result",
|
|
416
|
+
);
|
|
417
|
+
return EMPTY_REDUCER_RESULT;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const timeContexts: TimeContextOp[] = [];
|
|
421
|
+
if (hasTimeContexts) {
|
|
422
|
+
for (const item of obj.timeContexts as unknown[]) {
|
|
423
|
+
const validated = validateTimeContextOp(item);
|
|
424
|
+
if (validated) timeContexts.push(validated);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const openLoops: OpenLoopOp[] = [];
|
|
429
|
+
if (hasOpenLoops) {
|
|
430
|
+
for (const item of obj.openLoops as unknown[]) {
|
|
431
|
+
const validated = validateOpenLoopOp(item);
|
|
432
|
+
if (validated) openLoops.push(validated);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const archiveObservations: ArchiveObservationCandidate[] = [];
|
|
437
|
+
if (hasArchiveObservations) {
|
|
438
|
+
for (const item of obj.archiveObservations as unknown[]) {
|
|
439
|
+
const validated = validateArchiveObservation(item);
|
|
440
|
+
if (validated) archiveObservations.push(validated);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const archiveEpisodes: ArchiveEpisodeCandidate[] = [];
|
|
445
|
+
if (hasArchiveEpisodes) {
|
|
446
|
+
for (const item of obj.archiveEpisodes as unknown[]) {
|
|
447
|
+
const validated = validateArchiveEpisode(item);
|
|
448
|
+
if (validated) archiveEpisodes.push(validated);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { timeContexts, openLoops, archiveObservations, archiveEpisodes };
|
|
453
|
+
}
|