@vellumai/assistant 0.5.0 → 0.5.2
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 +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- 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__/assistant-feature-flags-integration.test.ts +7 -9
- 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 +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -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-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-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
- 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__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/filesystem-tools.test.ts +4 -2
- 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 +103 -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__/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-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- 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__/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-feature-flags-integration.test.ts +18 -17
- package/src/__tests__/skill-feature-flags.test.ts +13 -13
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
- 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__/system-prompt.test.ts +8 -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__/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/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 +16 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- 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 +46 -42
- package/src/daemon/conversation-agent-loop.ts +56 -19
- 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 +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/history-repair.ts +28 -8
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -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/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/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- 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 +28 -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 +9 -7
- 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 +1 -4
- 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/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- 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 +97 -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 +62 -19
- package/src/prompts/system-prompt.ts +2 -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 +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -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 +4 -0
- 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 +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- 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/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +4 -21
- package/src/tools/skills/execute.ts +1 -1
- 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 +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- 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/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 +10 -0
- 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,328 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock state
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const existsSyncFn = mock((_path: string): boolean => false);
|
|
8
|
+
const readFileSyncFn = mock((_path: string, _enc: string): string => "");
|
|
9
|
+
const writeFileSyncFn = mock((_path: string, _data: string): void => undefined);
|
|
10
|
+
const randomUUIDFn = mock((): string => "generated-uuid-1234");
|
|
11
|
+
const getMemoryCheckpointFn = mock((_key: string): string | null => null);
|
|
12
|
+
const deleteMemoryCheckpointFn = mock((_key: string): void => undefined);
|
|
13
|
+
const getExternalAssistantIdFn = mock((): string | undefined => "my-assistant");
|
|
14
|
+
const homedirFn = mock((): string => "/mock-home");
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Mock modules — before importing module under test
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
mock.module("node:fs", () => ({
|
|
21
|
+
existsSync: existsSyncFn,
|
|
22
|
+
readFileSync: readFileSyncFn,
|
|
23
|
+
writeFileSync: writeFileSyncFn,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module("node:crypto", () => ({
|
|
27
|
+
randomUUID: randomUUIDFn,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mock.module("node:os", () => ({
|
|
31
|
+
homedir: homedirFn,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mock.module("../memory/checkpoints.js", () => ({
|
|
35
|
+
getMemoryCheckpoint: getMemoryCheckpointFn,
|
|
36
|
+
deleteMemoryCheckpoint: deleteMemoryCheckpointFn,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module("../runtime/auth/external-assistant-id.js", () => ({
|
|
40
|
+
getExternalAssistantId: getExternalAssistantIdFn,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// Import after mocking
|
|
44
|
+
import { backfillInstallationIdMigration } from "../workspace/migrations/002-backfill-installation-id.js";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const BASE = "/mock-home";
|
|
51
|
+
const LOCK_PATH = `${BASE}/.vellum.lock.json`;
|
|
52
|
+
const LEGACY_LOCK_PATH = `${BASE}/.vellum.lockfile.json`;
|
|
53
|
+
const WORKSPACE_DIR = `${BASE}/.vellum/workspace`;
|
|
54
|
+
|
|
55
|
+
function makeLockfile(assistants: Array<Record<string, unknown>>): string {
|
|
56
|
+
return JSON.stringify({ assistants });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setupFs(fileContents: Record<string, string>): void {
|
|
60
|
+
existsSyncFn.mockImplementation((path: string) => path in fileContents);
|
|
61
|
+
readFileSyncFn.mockImplementation((path: string, _enc: string) => {
|
|
62
|
+
if (path in fileContents) return fileContents[path];
|
|
63
|
+
throw new Error(`ENOENT: ${path}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Tests
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe("002-backfill-installation-id migration", () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
existsSyncFn.mockClear();
|
|
74
|
+
readFileSyncFn.mockClear();
|
|
75
|
+
writeFileSyncFn.mockClear();
|
|
76
|
+
randomUUIDFn.mockClear();
|
|
77
|
+
getMemoryCheckpointFn.mockClear();
|
|
78
|
+
deleteMemoryCheckpointFn.mockClear();
|
|
79
|
+
getExternalAssistantIdFn.mockClear();
|
|
80
|
+
homedirFn.mockClear();
|
|
81
|
+
|
|
82
|
+
// Defaults
|
|
83
|
+
homedirFn.mockReturnValue("/mock-home");
|
|
84
|
+
getExternalAssistantIdFn.mockReturnValue("my-assistant");
|
|
85
|
+
getMemoryCheckpointFn.mockReturnValue(null);
|
|
86
|
+
randomUUIDFn.mockReturnValue("generated-uuid-1234");
|
|
87
|
+
delete process.env.BASE_DATA_DIR;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("no-op when no lockfile exists", () => {
|
|
91
|
+
setupFs({});
|
|
92
|
+
|
|
93
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
94
|
+
|
|
95
|
+
expect(writeFileSyncFn).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("no-op when lockfile has no assistants array", () => {
|
|
99
|
+
setupFs({
|
|
100
|
+
[LOCK_PATH]: JSON.stringify({ version: 1 }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
104
|
+
|
|
105
|
+
expect(writeFileSyncFn).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("no-op when lockfile is malformed JSON", () => {
|
|
109
|
+
setupFs({
|
|
110
|
+
[LOCK_PATH]: "{{not json",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
114
|
+
|
|
115
|
+
expect(writeFileSyncFn).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("no-op when lockfile is an array", () => {
|
|
119
|
+
setupFs({
|
|
120
|
+
[LOCK_PATH]: JSON.stringify([1, 2, 3]),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
124
|
+
|
|
125
|
+
expect(writeFileSyncFn).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("no-op when no matching assistant entry found", () => {
|
|
129
|
+
getExternalAssistantIdFn.mockReturnValue("my-assistant");
|
|
130
|
+
setupFs({
|
|
131
|
+
[LOCK_PATH]: makeLockfile([
|
|
132
|
+
{ assistantId: "other-assistant", installationId: undefined },
|
|
133
|
+
]),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
137
|
+
|
|
138
|
+
expect(writeFileSyncFn).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("backfills installationId from SQLite checkpoint", () => {
|
|
142
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-install-id");
|
|
143
|
+
setupFs({
|
|
144
|
+
[LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
148
|
+
|
|
149
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
150
|
+
const [path, data] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
151
|
+
expect(path).toBe(LOCK_PATH);
|
|
152
|
+
const parsed = JSON.parse(data);
|
|
153
|
+
expect(parsed.assistants[0].installationId).toBe("sqlite-install-id");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("generates new UUID when no SQLite checkpoint exists", () => {
|
|
157
|
+
getMemoryCheckpointFn.mockReturnValue(null);
|
|
158
|
+
randomUUIDFn.mockReturnValue("new-uuid-5678");
|
|
159
|
+
setupFs({
|
|
160
|
+
[LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
164
|
+
|
|
165
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
166
|
+
const [, data] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
167
|
+
const parsed = JSON.parse(data);
|
|
168
|
+
expect(parsed.assistants[0].installationId).toBe("new-uuid-5678");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("generates new UUID when SQLite table does not exist", () => {
|
|
172
|
+
getMemoryCheckpointFn.mockImplementation(() => {
|
|
173
|
+
throw new Error("no such table: memory_checkpoints");
|
|
174
|
+
});
|
|
175
|
+
randomUUIDFn.mockReturnValue("fallback-uuid");
|
|
176
|
+
setupFs({
|
|
177
|
+
[LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
181
|
+
|
|
182
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
183
|
+
const [, data] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
184
|
+
const parsed = JSON.parse(data);
|
|
185
|
+
expect(parsed.assistants[0].installationId).toBe("fallback-uuid");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("skips lockfile write when entry already has installationId", () => {
|
|
189
|
+
setupFs({
|
|
190
|
+
[LOCK_PATH]: makeLockfile([
|
|
191
|
+
{ assistantId: "my-assistant", installationId: "existing-id" },
|
|
192
|
+
]),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
196
|
+
|
|
197
|
+
expect(writeFileSyncFn).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("cleans up SQLite checkpoint when entry already has installationId", () => {
|
|
201
|
+
setupFs({
|
|
202
|
+
[LOCK_PATH]: makeLockfile([
|
|
203
|
+
{ assistantId: "my-assistant", installationId: "existing-id" },
|
|
204
|
+
]),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
208
|
+
|
|
209
|
+
expect(deleteMemoryCheckpointFn).toHaveBeenCalledWith(
|
|
210
|
+
"telemetry:installation_id",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("cleans up SQLite checkpoint after writing lockfile", () => {
|
|
215
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-id");
|
|
216
|
+
setupFs({
|
|
217
|
+
[LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
221
|
+
|
|
222
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
223
|
+
expect(deleteMemoryCheckpointFn).toHaveBeenCalledWith(
|
|
224
|
+
"telemetry:installation_id",
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("handles deleteMemoryCheckpoint throwing gracefully", () => {
|
|
229
|
+
deleteMemoryCheckpointFn.mockImplementation(() => {
|
|
230
|
+
throw new Error("no such table");
|
|
231
|
+
});
|
|
232
|
+
setupFs({
|
|
233
|
+
[LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Should not throw
|
|
237
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
238
|
+
|
|
239
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("reads from legacy .vellum.lockfile.json when primary is absent", () => {
|
|
243
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-id");
|
|
244
|
+
setupFs({
|
|
245
|
+
[LEGACY_LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
249
|
+
|
|
250
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
251
|
+
const [path, data] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
252
|
+
expect(path).toBe(LEGACY_LOCK_PATH);
|
|
253
|
+
const parsed = JSON.parse(data);
|
|
254
|
+
expect(parsed.assistants[0].installationId).toBe("sqlite-id");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("prefers primary lockfile over legacy when both exist", () => {
|
|
258
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-id");
|
|
259
|
+
setupFs({
|
|
260
|
+
[LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
261
|
+
[LEGACY_LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
265
|
+
|
|
266
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
267
|
+
const [path] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
268
|
+
expect(path).toBe(LOCK_PATH);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("falls through to legacy lockfile when primary is malformed", () => {
|
|
272
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-id");
|
|
273
|
+
setupFs({
|
|
274
|
+
[LOCK_PATH]: "{{not json",
|
|
275
|
+
[LEGACY_LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
279
|
+
|
|
280
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
281
|
+
const [path, data] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
282
|
+
expect(path).toBe(LEGACY_LOCK_PATH);
|
|
283
|
+
const parsed = JSON.parse(data);
|
|
284
|
+
expect(parsed.assistants[0].installationId).toBe("sqlite-id");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("respects BASE_DATA_DIR environment variable", () => {
|
|
288
|
+
process.env.BASE_DATA_DIR = "/custom-base";
|
|
289
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-id");
|
|
290
|
+
|
|
291
|
+
const customLockPath = "/custom-base/.vellum.lock.json";
|
|
292
|
+
setupFs({
|
|
293
|
+
[customLockPath]: makeLockfile([{ assistantId: "my-assistant" }]),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
297
|
+
|
|
298
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
299
|
+
const [path] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
300
|
+
expect(path).toBe(customLockPath);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("preserves other assistants in lockfile when writing", () => {
|
|
304
|
+
getMemoryCheckpointFn.mockReturnValue("sqlite-id");
|
|
305
|
+
setupFs({
|
|
306
|
+
[LOCK_PATH]: JSON.stringify({
|
|
307
|
+
assistants: [
|
|
308
|
+
{ assistantId: "other-assistant", installationId: "other-id" },
|
|
309
|
+
{ assistantId: "my-assistant" },
|
|
310
|
+
],
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
backfillInstallationIdMigration.run(WORKSPACE_DIR);
|
|
315
|
+
|
|
316
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
|
|
317
|
+
const [, data] = writeFileSyncFn.mock.calls[0] as [string, string];
|
|
318
|
+
const parsed = JSON.parse(data);
|
|
319
|
+
expect(parsed.assistants[0].installationId).toBe("other-id");
|
|
320
|
+
expect(parsed.assistants[1].installationId).toBe("sqlite-id");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("has migration id 002-backfill-installation-id", () => {
|
|
324
|
+
expect(backfillInstallationIdMigration.id).toBe(
|
|
325
|
+
"002-backfill-installation-id",
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -10,7 +10,7 @@ const writeFileSyncFn = mock(
|
|
|
10
10
|
(_path: string, _data: string, _opts?: object) => {},
|
|
11
11
|
);
|
|
12
12
|
const mkdirSyncFn = mock((_path: string, _opts?: object) => {});
|
|
13
|
-
const
|
|
13
|
+
const getDeviceIdBaseDirFn = mock((): string => "/mock-home");
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Mock modules — before importing module under test
|
|
@@ -23,12 +23,8 @@ mock.module("node:fs", () => ({
|
|
|
23
23
|
mkdirSync: mkdirSyncFn,
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
mock.module("
|
|
27
|
-
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
mock.module("../config/env-registry.js", () => ({
|
|
31
|
-
getBaseDataDir: getBaseDataDirFn,
|
|
26
|
+
mock.module("../util/device-id.js", () => ({
|
|
27
|
+
getDeviceIdBaseDir: getDeviceIdBaseDirFn,
|
|
32
28
|
}));
|
|
33
29
|
|
|
34
30
|
// Import after mocking
|
|
@@ -67,7 +63,7 @@ describe("003-seed-device-id migration", () => {
|
|
|
67
63
|
readFileSyncFn.mockClear();
|
|
68
64
|
writeFileSyncFn.mockClear();
|
|
69
65
|
mkdirSyncFn.mockClear();
|
|
70
|
-
|
|
66
|
+
getDeviceIdBaseDirFn.mockReturnValue("/mock-home");
|
|
71
67
|
});
|
|
72
68
|
|
|
73
69
|
test("no-op when device.json already has a deviceId", () => {
|
|
@@ -269,9 +265,9 @@ describe("003-seed-device-id migration", () => {
|
|
|
269
265
|
expect(parsed.deviceId).toBe("install-legacy");
|
|
270
266
|
});
|
|
271
267
|
|
|
272
|
-
test("respects BASE_DATA_DIR override", () => {
|
|
268
|
+
test("respects BASE_DATA_DIR override via getDeviceIdBaseDir", () => {
|
|
273
269
|
const customBase = "/custom-base";
|
|
274
|
-
|
|
270
|
+
getDeviceIdBaseDirFn.mockReturnValue(customBase);
|
|
275
271
|
|
|
276
272
|
const customLockPath = `${customBase}/.vellum.lock.json`;
|
|
277
273
|
const customDevicePath = `${customBase}/.vellum/device.json`;
|
package/src/agent/attachments.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentBlock } from "../providers/types.js";
|
|
1
|
+
import type { ContentBlock, Message } from "../providers/types.js";
|
|
2
2
|
|
|
3
3
|
export interface MessageAttachmentInput {
|
|
4
4
|
id?: string;
|
|
@@ -6,6 +6,7 @@ export interface MessageAttachmentInput {
|
|
|
6
6
|
mimeType: string;
|
|
7
7
|
data: string;
|
|
8
8
|
extractedText?: string;
|
|
9
|
+
filePath?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function attachmentsToContentBlocks(
|
|
@@ -35,3 +36,28 @@ export function attachmentsToContentBlocks(
|
|
|
35
36
|
} as ContentBlock;
|
|
36
37
|
});
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Return a copy of the message with text annotations for image source paths.
|
|
42
|
+
* The annotations are appended as a text content block so the LLM knows where
|
|
43
|
+
* the images came from on disk. The caller should persist the ORIGINAL message
|
|
44
|
+
* (without annotations) so the UI stays clean.
|
|
45
|
+
*/
|
|
46
|
+
export function enrichMessageWithSourcePaths(
|
|
47
|
+
message: Message,
|
|
48
|
+
attachments: MessageAttachmentInput[],
|
|
49
|
+
): Message {
|
|
50
|
+
const imageAttachments = attachments.filter(
|
|
51
|
+
(a) => a.mimeType.toLowerCase().startsWith("image/") && a.filePath,
|
|
52
|
+
);
|
|
53
|
+
if (imageAttachments.length === 0) return message;
|
|
54
|
+
|
|
55
|
+
const annotation = imageAttachments
|
|
56
|
+
.map((a) => `[Attached image source: ${a.filePath}]`)
|
|
57
|
+
.join("\n");
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...message,
|
|
61
|
+
content: [...message.content, { type: "text" as const, text: annotation }],
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -88,6 +88,7 @@ export type AgentEvent =
|
|
|
88
88
|
cacheCreationInputTokens?: number;
|
|
89
89
|
cacheReadInputTokens?: number;
|
|
90
90
|
model: string;
|
|
91
|
+
actualProvider?: string;
|
|
91
92
|
providerDurationMs: number;
|
|
92
93
|
rawRequest?: unknown;
|
|
93
94
|
rawResponse?: unknown;
|
|
@@ -100,6 +101,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
|
|
|
100
101
|
};
|
|
101
102
|
|
|
102
103
|
const PROGRESS_CHECK_INTERVAL = 5;
|
|
104
|
+
const MAX_CONSECUTIVE_ERROR_NUDGES = 3;
|
|
103
105
|
const PROGRESS_CHECK_REMINDER =
|
|
104
106
|
"You have been using tools for several turns. Check whether you are making meaningful progress toward the user's goal. If you are stuck in a loop or not making progress, summarize what you have tried and ask the user for guidance instead of continuing.";
|
|
105
107
|
|
|
@@ -200,6 +202,7 @@ export class AgentLoop {
|
|
|
200
202
|
const history = [...messages];
|
|
201
203
|
let toolUseTurns = 0;
|
|
202
204
|
let nudgedForEmptyResponse = false;
|
|
205
|
+
let consecutiveErrorTurns = 0;
|
|
203
206
|
let lastLlmCallTime = 0;
|
|
204
207
|
const rlog = requestId ? log.child({ requestId }) : log;
|
|
205
208
|
|
|
@@ -350,6 +353,7 @@ export class AgentLoop {
|
|
|
350
353
|
cacheCreationInputTokens: response.usage.cacheCreationInputTokens,
|
|
351
354
|
cacheReadInputTokens: response.usage.cacheReadInputTokens,
|
|
352
355
|
model: response.model,
|
|
356
|
+
actualProvider: response.actualProvider ?? this.provider.name,
|
|
353
357
|
providerDurationMs,
|
|
354
358
|
rawRequest: response.rawRequest,
|
|
355
359
|
rawResponse: response.rawResponse,
|
|
@@ -566,13 +570,37 @@ export class AgentLoop {
|
|
|
566
570
|
|
|
567
571
|
// Track tool-use turns and inject progress reminder every N turns
|
|
568
572
|
toolUseTurns++;
|
|
569
|
-
|
|
573
|
+
const isProgressCheckTurn =
|
|
574
|
+
toolUseTurns % PROGRESS_CHECK_INTERVAL === 0;
|
|
575
|
+
if (isProgressCheckTurn) {
|
|
570
576
|
resultBlocks.push({
|
|
571
577
|
type: "text",
|
|
572
578
|
text: `<system_notice>${PROGRESS_CHECK_REMINDER}</system_notice>`,
|
|
573
579
|
});
|
|
574
580
|
}
|
|
575
581
|
|
|
582
|
+
// When any tool returned an error, nudge the LLM to retry with
|
|
583
|
+
// corrected parameters instead of ending its turn. Skip the nudge
|
|
584
|
+
// when the progress check fires (to avoid contradictory instructions)
|
|
585
|
+
// and after MAX_CONSECUTIVE_ERROR_NUDGES consecutive error turns
|
|
586
|
+
// (the error is likely unrecoverable at that point).
|
|
587
|
+
const hasToolError = toolResults.some(({ result }) => result.isError);
|
|
588
|
+
if (hasToolError) {
|
|
589
|
+
consecutiveErrorTurns++;
|
|
590
|
+
} else {
|
|
591
|
+
consecutiveErrorTurns = 0;
|
|
592
|
+
}
|
|
593
|
+
if (
|
|
594
|
+
hasToolError &&
|
|
595
|
+
!isProgressCheckTurn &&
|
|
596
|
+
consecutiveErrorTurns <= MAX_CONSECUTIVE_ERROR_NUDGES
|
|
597
|
+
) {
|
|
598
|
+
resultBlocks.push({
|
|
599
|
+
type: "text",
|
|
600
|
+
text: "<system_notice>One or more tool calls returned an error. If the error looks recoverable (e.g. missing or invalid parameters), fix the parameters and retry. If the error is clearly unrecoverable (e.g. a service is down, a resource does not exist, or a permission is permanently denied), report it to the user.</system_notice>",
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
576
604
|
// Remind the LLM not to repeat text it already streamed
|
|
577
605
|
if (hasTextBlock) {
|
|
578
606
|
resultBlocks.push({
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { getLogger } from "../util/logger.js";
|
|
6
6
|
import { getWorkspaceDir } from "../util/platform.js";
|
|
7
7
|
import { renderCharacterAscii } from "./ascii-renderer.js";
|
|
8
|
+
import { getCharacterComponents } from "./character-components.js";
|
|
8
9
|
import { renderCharacterPng } from "./png-renderer.js";
|
|
9
10
|
|
|
10
11
|
const log = getLogger("traits-png-sync");
|
|
@@ -24,37 +25,56 @@ export type TraitsSyncResult =
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* Renders avatar
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Returns `true` if the ASCII sidecar was also written successfully.
|
|
28
|
+
* Renders avatar PNG and ASCII art into memory without touching the filesystem.
|
|
29
|
+
* Call this before any disk writes so a render failure leaves all files untouched.
|
|
31
30
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const pngPath = join(avatarDir, "avatar-image.png");
|
|
37
|
-
|
|
38
|
-
// Render PNG first — this validates trait IDs (composeSvg throws on
|
|
39
|
-
// unknown components), so we fail before writing anything to disk.
|
|
31
|
+
function renderAvatarBuffers(traits: CharacterTraits): {
|
|
32
|
+
pngBuffer: Buffer;
|
|
33
|
+
asciiArt: string | null;
|
|
34
|
+
} {
|
|
40
35
|
const pngBuffer = renderCharacterPng(
|
|
41
36
|
traits.bodyShape,
|
|
42
37
|
traits.eyeStyle,
|
|
43
38
|
traits.color,
|
|
44
39
|
);
|
|
45
|
-
const pngTmp = `${pngPath}.${randomUUID()}.tmp`;
|
|
46
|
-
writeFileSync(pngTmp, pngBuffer);
|
|
47
|
-
renameSync(pngTmp, pngPath);
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
// the primary operation to report failure.
|
|
41
|
+
let asciiArt: string | null = null;
|
|
51
42
|
try {
|
|
52
|
-
|
|
53
|
-
const asciiArt = renderCharacterAscii(
|
|
43
|
+
asciiArt = renderCharacterAscii(
|
|
54
44
|
traits.bodyShape,
|
|
55
45
|
traits.eyeStyle,
|
|
56
46
|
traits.color,
|
|
57
47
|
);
|
|
48
|
+
} catch (asciiErr) {
|
|
49
|
+
log.warn(
|
|
50
|
+
{ err: asciiErr },
|
|
51
|
+
"Failed to render ASCII art — will still write PNG and traits",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { pngBuffer, asciiArt };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Writes pre-rendered avatar files (PNG + optional ASCII) to disk atomically.
|
|
60
|
+
* Returns `true` if the ASCII sidecar was also written successfully.
|
|
61
|
+
*/
|
|
62
|
+
function writeAvatarFiles(
|
|
63
|
+
avatarDir: string,
|
|
64
|
+
pngBuffer: Buffer,
|
|
65
|
+
asciiArt: string | null,
|
|
66
|
+
): boolean {
|
|
67
|
+
const pngPath = join(avatarDir, "avatar-image.png");
|
|
68
|
+
const pngTmp = `${pngPath}.${randomUUID()}.tmp`;
|
|
69
|
+
writeFileSync(pngTmp, pngBuffer);
|
|
70
|
+
renameSync(pngTmp, pngPath);
|
|
71
|
+
|
|
72
|
+
if (asciiArt == null) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const asciiPath = join(avatarDir, "character-ascii.txt");
|
|
58
78
|
const asciiTmp = `${asciiPath}.${randomUUID()}.tmp`;
|
|
59
79
|
writeFileSync(asciiTmp, asciiArt);
|
|
60
80
|
renameSync(asciiTmp, asciiPath);
|
|
@@ -73,8 +93,10 @@ function renderAndWriteAvatarFiles(
|
|
|
73
93
|
* character-ascii.txt in one atomic operation. Accepts the trait values
|
|
74
94
|
* directly so callers don't need to touch the filesystem first.
|
|
75
95
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
96
|
+
* Validates trait IDs against the component set, then renders into memory
|
|
97
|
+
* before any disk writes. Writes the traits file first, then the rendered
|
|
98
|
+
* avatar files, so a render failure leaves all files untouched and a disk
|
|
99
|
+
* failure after traits are written never leaves the PNG ahead of the traits.
|
|
78
100
|
*/
|
|
79
101
|
export function writeTraitsAndRenderAvatar(
|
|
80
102
|
traits: CharacterTraits,
|
|
@@ -94,22 +116,55 @@ export function writeTraitsAndRenderAvatar(
|
|
|
94
116
|
};
|
|
95
117
|
}
|
|
96
118
|
|
|
119
|
+
// Validate trait IDs against the known component set so that unknown values
|
|
120
|
+
// are surfaced as input-validation errors (400) rather than server errors (500).
|
|
121
|
+
const components = getCharacterComponents();
|
|
122
|
+
const validBodyShapes = components.bodyShapes.map((b) => b.id);
|
|
123
|
+
if (!validBodyShapes.includes(traits.bodyShape)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: "invalid_traits",
|
|
127
|
+
message: `Unknown body shape: "${traits.bodyShape}". Valid IDs: ${validBodyShapes.join(", ")}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const validEyeStyles = components.eyeStyles.map((e) => e.id);
|
|
131
|
+
if (!validEyeStyles.includes(traits.eyeStyle)) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: "invalid_traits",
|
|
135
|
+
message: `Unknown eye style: "${traits.eyeStyle}". Valid IDs: ${validEyeStyles.join(", ")}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const validColors = components.colors.map((c) => c.id);
|
|
139
|
+
if (!validColors.includes(traits.color)) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
reason: "invalid_traits",
|
|
143
|
+
message: `Unknown color: "${traits.color}". Valid IDs: ${validColors.join(", ")}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
97
147
|
const avatarDir = join(getWorkspaceDir(), "data", "avatar");
|
|
98
148
|
const traitsPath = join(avatarDir, "character-traits.json");
|
|
99
149
|
|
|
100
150
|
try {
|
|
101
151
|
mkdirSync(avatarDir, { recursive: true });
|
|
102
152
|
|
|
103
|
-
// Render
|
|
104
|
-
//
|
|
105
|
-
const
|
|
153
|
+
// Phase 1: Render everything into memory — no disk writes yet.
|
|
154
|
+
// If rendering fails, all files remain untouched.
|
|
155
|
+
const { pngBuffer, asciiArt } = renderAvatarBuffers(traits);
|
|
106
156
|
|
|
107
|
-
// Write traits file atomically
|
|
157
|
+
// Phase 2: Write traits file atomically first.
|
|
108
158
|
const traitsJson = JSON.stringify(traits, null, 2);
|
|
109
159
|
const traitsTmp = `${traitsPath}.${randomUUID()}.tmp`;
|
|
110
160
|
writeFileSync(traitsTmp, traitsJson);
|
|
111
161
|
renameSync(traitsTmp, traitsPath);
|
|
112
162
|
|
|
163
|
+
// Phase 3: Write rendered avatar files to disk.
|
|
164
|
+
// Traits are already committed, so a failure here leaves traits ahead of
|
|
165
|
+
// the PNG — acceptable because the next render call will reconcile them.
|
|
166
|
+
const asciiWritten = writeAvatarFiles(avatarDir, pngBuffer, asciiArt);
|
|
167
|
+
|
|
113
168
|
log.info(
|
|
114
169
|
{
|
|
115
170
|
bodyShape: traits.bodyShape,
|
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import archiver from "archiver";
|
|
15
15
|
import JSZip from "jszip";
|
|
16
16
|
|
|
17
|
-
import { getApp,
|
|
17
|
+
import { getApp, getAppDirPath, isMultifileApp } from "../memory/app-store.js";
|
|
18
18
|
import { computeContentId } from "../util/content-id.js";
|
|
19
19
|
import { getLogger } from "../util/logger.js";
|
|
20
20
|
import { compileApp } from "./app-compiler.js";
|
|
@@ -79,7 +79,7 @@ export async function packageApp(
|
|
|
79
79
|
// Compile the app and bundle the output.
|
|
80
80
|
const compiledFiles: { name: string; data: Buffer }[] = [];
|
|
81
81
|
|
|
82
|
-
const appDir =
|
|
82
|
+
const appDir = getAppDirPath(appId);
|
|
83
83
|
|
|
84
84
|
if (multifile) {
|
|
85
85
|
// Multi-file TSX app: compile src/ -> dist/
|
|
@@ -154,7 +154,7 @@ export async function packageApp(
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Include app icon if one was generated
|
|
157
|
-
const iconPath = join(
|
|
157
|
+
const iconPath = join(getAppDirPath(appId), "icon.png");
|
|
158
158
|
if (existsSync(iconPath)) {
|
|
159
159
|
archive.append(readFileSync(iconPath), { name: "icon.png" });
|
|
160
160
|
}
|
|
@@ -202,7 +202,7 @@ export async function packageApp(
|
|
|
202
202
|
|
|
203
203
|
// Read icon for inclusion in the response
|
|
204
204
|
let iconImageBase64: string | undefined;
|
|
205
|
-
const iconFilePath = join(
|
|
205
|
+
const iconFilePath = join(getAppDirPath(appId), "icon.png");
|
|
206
206
|
if (existsSync(iconFilePath)) {
|
|
207
207
|
iconImageBase64 = readFileSync(iconFilePath).toString("base64");
|
|
208
208
|
}
|
package/src/calls/call-domain.ts
CHANGED