vellum 0.2.1 → 0.2.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/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +299 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +62 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +163 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -3,8 +3,10 @@ import type { Message } from '../providers/types.js';
|
|
|
3
3
|
import {
|
|
4
4
|
applyRuntimeInjections,
|
|
5
5
|
injectChannelCapabilityContext,
|
|
6
|
+
injectTemporalContext,
|
|
6
7
|
resolveChannelCapabilities,
|
|
7
8
|
stripChannelCapabilityContext,
|
|
9
|
+
stripTemporalContext,
|
|
8
10
|
} from '../daemon/session-runtime-assembly.js';
|
|
9
11
|
import type { ChannelCapabilities } from '../daemon/session-runtime-assembly.js';
|
|
10
12
|
import { buildChannelAwarenessSection } from '../config/system-prompt.js';
|
|
@@ -313,3 +315,162 @@ describe('trust-gating via channel capabilities', () => {
|
|
|
313
315
|
expect(injected).toContain('desktop app');
|
|
314
316
|
});
|
|
315
317
|
});
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// injectTemporalContext
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
describe('injectTemporalContext', () => {
|
|
324
|
+
const baseUserMessage: Message = {
|
|
325
|
+
role: 'user',
|
|
326
|
+
content: [{ type: 'text', text: 'Plan a trip for next weekend' }],
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const sampleContext = '<temporal_context>\nToday: 2026-02-18 (Wednesday)\nTimezone: UTC\n</temporal_context>';
|
|
330
|
+
|
|
331
|
+
test('prepends temporal context block to user message', () => {
|
|
332
|
+
const result = injectTemporalContext(baseUserMessage, sampleContext);
|
|
333
|
+
expect(result.content.length).toBe(2);
|
|
334
|
+
const injected = result.content[0];
|
|
335
|
+
expect(injected.type).toBe('text');
|
|
336
|
+
expect((injected as { type: 'text'; text: string }).text).toContain('<temporal_context>');
|
|
337
|
+
expect((injected as { type: 'text'; text: string }).text).toContain('2026-02-18');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('preserves original message content', () => {
|
|
341
|
+
const result = injectTemporalContext(baseUserMessage, sampleContext);
|
|
342
|
+
const lastBlock = result.content[result.content.length - 1];
|
|
343
|
+
expect((lastBlock as { type: 'text'; text: string }).text).toBe('Plan a trip for next weekend');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// stripTemporalContext
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
describe('stripTemporalContext', () => {
|
|
352
|
+
test('strips temporal_context blocks from user messages', () => {
|
|
353
|
+
const messages: Message[] = [
|
|
354
|
+
{
|
|
355
|
+
role: 'user',
|
|
356
|
+
content: [
|
|
357
|
+
{ type: 'text', text: '<temporal_context>\nToday: 2026-02-18\n</temporal_context>' },
|
|
358
|
+
{ type: 'text', text: 'Hello' },
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
role: 'assistant',
|
|
363
|
+
content: [{ type: 'text', text: 'Hi there' }],
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const result = stripTemporalContext(messages);
|
|
368
|
+
|
|
369
|
+
expect(result.length).toBe(2);
|
|
370
|
+
expect(result[0].content.length).toBe(1);
|
|
371
|
+
expect((result[0].content[0] as { type: 'text'; text: string }).text).toBe('Hello');
|
|
372
|
+
// Assistant message untouched
|
|
373
|
+
expect(result[1].content.length).toBe(1);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('removes user messages that only contain temporal_context', () => {
|
|
377
|
+
const messages: Message[] = [
|
|
378
|
+
{
|
|
379
|
+
role: 'user',
|
|
380
|
+
content: [
|
|
381
|
+
{ type: 'text', text: '<temporal_context>\nToday: 2026-02-18\n</temporal_context>' },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
const result = stripTemporalContext(messages);
|
|
387
|
+
expect(result.length).toBe(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('does not touch unrelated blocks', () => {
|
|
391
|
+
const messages: Message[] = [
|
|
392
|
+
{
|
|
393
|
+
role: 'user',
|
|
394
|
+
content: [
|
|
395
|
+
{ type: 'text', text: '<channel_capabilities>\nchannel: dashboard\n</channel_capabilities>' },
|
|
396
|
+
{ type: 'text', text: 'Hello' },
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
const result = stripTemporalContext(messages);
|
|
402
|
+
expect(result.length).toBe(1);
|
|
403
|
+
expect(result[0]).toBe(messages[0]); // Same reference — untouched
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('leaves messages without temporal_context untouched', () => {
|
|
407
|
+
const messages: Message[] = [
|
|
408
|
+
{
|
|
409
|
+
role: 'user',
|
|
410
|
+
content: [{ type: 'text', text: 'Normal message' }],
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const result = stripTemporalContext(messages);
|
|
415
|
+
expect(result.length).toBe(1);
|
|
416
|
+
expect(result[0]).toBe(messages[0]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('preserves user-authored text that starts with <temporal_context> but not the injected prefix', () => {
|
|
420
|
+
const messages: Message[] = [
|
|
421
|
+
{
|
|
422
|
+
role: 'user',
|
|
423
|
+
content: [
|
|
424
|
+
{ type: 'text', text: '<temporal_context>some user XML content</temporal_context>' },
|
|
425
|
+
{ type: 'text', text: 'Hello' },
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const result = stripTemporalContext(messages);
|
|
431
|
+
expect(result.length).toBe(1);
|
|
432
|
+
expect(result[0]).toBe(messages[0]); // Same reference — untouched
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// applyRuntimeInjections with temporalContext
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
describe('applyRuntimeInjections with temporalContext', () => {
|
|
441
|
+
const baseMessages: Message[] = [
|
|
442
|
+
{
|
|
443
|
+
role: 'user',
|
|
444
|
+
content: [{ type: 'text', text: 'When is next weekend?' }],
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const sampleContext = '<temporal_context>\nToday: 2026-02-18 (Wednesday)\n</temporal_context>';
|
|
449
|
+
|
|
450
|
+
test('injects temporal context when provided', () => {
|
|
451
|
+
const result = applyRuntimeInjections(baseMessages, {
|
|
452
|
+
temporalContext: sampleContext,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(result.length).toBe(1);
|
|
456
|
+
expect(result[0].content.length).toBe(2);
|
|
457
|
+
const injected = result[0].content[0];
|
|
458
|
+
expect((injected as { type: 'text'; text: string }).text).toContain('<temporal_context>');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('does not inject when temporalContext is null', () => {
|
|
462
|
+
const result = applyRuntimeInjections(baseMessages, {
|
|
463
|
+
temporalContext: null,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(result.length).toBe(1);
|
|
467
|
+
expect(result[0].content.length).toBe(1);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('does not inject when temporalContext is omitted', () => {
|
|
471
|
+
const result = applyRuntimeInjections(baseMessages, {});
|
|
472
|
+
|
|
473
|
+
expect(result.length).toBe(1);
|
|
474
|
+
expect(result[0].content.length).toBe(1);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type {
|
|
3
|
+
CardSurfaceData,
|
|
4
|
+
ServerMessage,
|
|
5
|
+
SurfaceData,
|
|
6
|
+
SurfaceType,
|
|
7
|
+
UiSurfaceShow,
|
|
8
|
+
UiSurfaceUpdate,
|
|
9
|
+
} from '../daemon/ipc-protocol.js';
|
|
10
|
+
import {
|
|
11
|
+
surfaceProxyResolver,
|
|
12
|
+
type SurfaceSessionContext,
|
|
13
|
+
} from '../daemon/session-surfaces.js';
|
|
14
|
+
|
|
15
|
+
function makeContext(sent: ServerMessage[] = []): SurfaceSessionContext {
|
|
16
|
+
return {
|
|
17
|
+
conversationId: 'session-1',
|
|
18
|
+
traceEmitter: { emit: () => {} },
|
|
19
|
+
sendToClient: (msg) => sent.push(msg),
|
|
20
|
+
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
21
|
+
lastSurfaceAction: new Map<string, { actionId: string; data?: Record<string, unknown> }>(),
|
|
22
|
+
surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>(),
|
|
23
|
+
surfaceUndoStacks: new Map<string, string[]>(),
|
|
24
|
+
currentTurnSurfaces: [],
|
|
25
|
+
isProcessing: () => false,
|
|
26
|
+
enqueueMessage: () => ({ queued: false, requestId: 'req-1' }),
|
|
27
|
+
getQueueDepth: () => 0,
|
|
28
|
+
processMessage: async () => 'ok',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('task_progress surface compatibility', () => {
|
|
33
|
+
test('ui_show maps legacy top-level task_progress fields into card data', async () => {
|
|
34
|
+
const sent: ServerMessage[] = [];
|
|
35
|
+
const ctx = makeContext(sent);
|
|
36
|
+
|
|
37
|
+
const result = await surfaceProxyResolver(ctx, 'ui_show', {
|
|
38
|
+
surface_type: 'card',
|
|
39
|
+
title: 'Ordering from DoorDash',
|
|
40
|
+
data: {},
|
|
41
|
+
template: 'task_progress',
|
|
42
|
+
templateData: {
|
|
43
|
+
status: 'in_progress',
|
|
44
|
+
steps: [
|
|
45
|
+
{ label: 'Search restaurants', status: 'in_progress' },
|
|
46
|
+
{ label: 'Browse menu', status: 'pending' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.isError).toBe(false);
|
|
52
|
+
|
|
53
|
+
const showMessage = sent.find((msg): msg is UiSurfaceShow => msg.type === 'ui_surface_show');
|
|
54
|
+
expect(showMessage).toBeDefined();
|
|
55
|
+
if (!showMessage || showMessage.surfaceType !== 'card') return;
|
|
56
|
+
|
|
57
|
+
const card = showMessage.data as CardSurfaceData;
|
|
58
|
+
expect(card.template).toBe('task_progress');
|
|
59
|
+
expect(card.title).toBe('Ordering from DoorDash');
|
|
60
|
+
expect(card.body).toBe('');
|
|
61
|
+
expect((card.templateData as Record<string, unknown>).status).toBe('in_progress');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('ui_update normalizes top-level task_progress fields into templateData', async () => {
|
|
65
|
+
const sent: ServerMessage[] = [];
|
|
66
|
+
const ctx = makeContext(sent);
|
|
67
|
+
const existingCard: CardSurfaceData = {
|
|
68
|
+
title: 'Ordering from DoorDash',
|
|
69
|
+
body: '',
|
|
70
|
+
template: 'task_progress',
|
|
71
|
+
templateData: {
|
|
72
|
+
title: 'Ordering from DoorDash',
|
|
73
|
+
status: 'in_progress',
|
|
74
|
+
steps: [
|
|
75
|
+
{ label: 'Search restaurants', status: 'completed' },
|
|
76
|
+
{ label: 'Browse menu', status: 'in_progress' },
|
|
77
|
+
{ label: 'Add to cart', status: 'pending' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ctx.surfaceState.set('surface-1', { surfaceType: 'card', data: existingCard });
|
|
83
|
+
|
|
84
|
+
const result = await surfaceProxyResolver(ctx, 'ui_update', {
|
|
85
|
+
surface_id: 'surface-1',
|
|
86
|
+
data: {
|
|
87
|
+
status: 'completed',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.isError).toBe(false);
|
|
92
|
+
|
|
93
|
+
const updateMessage = sent.find((msg): msg is UiSurfaceUpdate => msg.type === 'ui_surface_update');
|
|
94
|
+
expect(updateMessage).toBeDefined();
|
|
95
|
+
if (!updateMessage) return;
|
|
96
|
+
|
|
97
|
+
const updatedCard = updateMessage.data as CardSurfaceData & Record<string, unknown>;
|
|
98
|
+
expect(updatedCard.template).toBe('task_progress');
|
|
99
|
+
expect('status' in updatedCard).toBe(false);
|
|
100
|
+
const templateData = updatedCard.templateData as Record<string, unknown>;
|
|
101
|
+
expect(templateData.status).toBe('completed');
|
|
102
|
+
expect(Array.isArray(templateData.steps)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -48,7 +48,7 @@ const STORE_PATH = join(testDir, 'keys.enc');
|
|
|
48
48
|
// ── Imports (after mocks) ───────────────────────────────────────────
|
|
49
49
|
|
|
50
50
|
import { createMockSignupServer, type MockSignupServer } from './fixtures/mock-signup-server.js';
|
|
51
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
51
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
52
52
|
import {
|
|
53
53
|
createAccount,
|
|
54
54
|
listAccounts,
|
|
@@ -97,6 +97,7 @@ beforeAll(async () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
afterAll(async () => {
|
|
100
|
+
resetDb();
|
|
100
101
|
await executeBrowserClose({ close_all_pages: true }, ctx);
|
|
101
102
|
await server.stop();
|
|
102
103
|
_setMetadataPath(null);
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Projection Benchmark
|
|
3
|
+
*
|
|
4
|
+
* Measures projectSkillTools() latency across conversation sizes and caching scenarios.
|
|
5
|
+
*
|
|
6
|
+
* Baseline targets:
|
|
7
|
+
* - Cold projection (100 msgs / 3 skills): < 50ms
|
|
8
|
+
* - Cached projection (no change): < 10ms
|
|
9
|
+
* - Cold projection (1000 msgs / 5 skills): < 100ms
|
|
10
|
+
* - Incremental scan (10 new msgs): < 20ms
|
|
11
|
+
*/
|
|
12
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
13
|
+
import type { Message } from '../providers/types.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Mocks — must be registered before importing the module under test
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
mock.module('../util/logger.js', () => ({
|
|
20
|
+
getLogger: () =>
|
|
21
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Skill catalog: returns a configurable list of fake skills
|
|
25
|
+
let catalogSkillIds: string[] = [];
|
|
26
|
+
mock.module('../config/skills.js', () => ({
|
|
27
|
+
loadSkillCatalog: () =>
|
|
28
|
+
catalogSkillIds.map((id) => ({
|
|
29
|
+
id,
|
|
30
|
+
name: id,
|
|
31
|
+
description: `Mock skill ${id}`,
|
|
32
|
+
directoryPath: `/tmp/fake-skills/${id}`,
|
|
33
|
+
skillFilePath: `/tmp/fake-skills/${id}/SKILL.md`,
|
|
34
|
+
bundled: false,
|
|
35
|
+
userInvocable: false,
|
|
36
|
+
})),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module('../skills/tool-manifest.js', () => ({
|
|
40
|
+
parseToolManifestFile: (path: string) => {
|
|
41
|
+
// Extract skill id from the path /tmp/fake-skills/<id>/TOOLS.json
|
|
42
|
+
const parts = path.split('/');
|
|
43
|
+
const skillId = parts[parts.length - 2];
|
|
44
|
+
return {
|
|
45
|
+
version: 1,
|
|
46
|
+
tools: [
|
|
47
|
+
{
|
|
48
|
+
name: `${skillId}_tool_a`,
|
|
49
|
+
description: `Tool A for ${skillId}`,
|
|
50
|
+
input_schema: { type: 'object', properties: {} },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: `${skillId}_tool_b`,
|
|
54
|
+
description: `Tool B for ${skillId}`,
|
|
55
|
+
input_schema: { type: 'object', properties: {} },
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
mock.module('../skills/version-hash.js', () => ({
|
|
63
|
+
computeSkillVersionHash: () => 'v1:deadbeef',
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Mock createSkillToolsFromManifest to return lightweight Tool-like objects
|
|
67
|
+
mock.module('../tools/skills/skill-tool-factory.js', () => ({
|
|
68
|
+
createSkillToolsFromManifest: (
|
|
69
|
+
entries: Array<{ name: string; description: string; input_schema: object }>,
|
|
70
|
+
skillId: string,
|
|
71
|
+
_skillDir: string,
|
|
72
|
+
versionHash: string,
|
|
73
|
+
bundled?: boolean,
|
|
74
|
+
) =>
|
|
75
|
+
entries.map((e) => ({
|
|
76
|
+
name: e.name,
|
|
77
|
+
description: e.description,
|
|
78
|
+
category: 'skill',
|
|
79
|
+
defaultRiskLevel: 'low',
|
|
80
|
+
origin: 'skill' as const,
|
|
81
|
+
ownerSkillId: skillId,
|
|
82
|
+
ownerSkillVersionHash: versionHash,
|
|
83
|
+
ownerSkillBundled: bundled,
|
|
84
|
+
getDefinition: () => ({
|
|
85
|
+
name: e.name,
|
|
86
|
+
description: e.description,
|
|
87
|
+
input_schema: e.input_schema,
|
|
88
|
+
}),
|
|
89
|
+
execute: async () => ({ content: '', isError: false }),
|
|
90
|
+
})),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// existsSync mock — TOOLS.json always exists for fake skills
|
|
94
|
+
mock.module('node:fs', () => ({
|
|
95
|
+
existsSync: () => true,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
mock.module('../tools/registry.js', () => ({
|
|
99
|
+
registerSkillTools: () => {},
|
|
100
|
+
unregisterSkillTools: () => {},
|
|
101
|
+
getTool: () => undefined,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Import module under test (after mocks)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const { projectSkillTools } = await import('../daemon/session-skill-tools.js');
|
|
109
|
+
type SkillProjectionCache = import('../daemon/session-skill-tools.js').SkillProjectionCache;
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Helpers
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build a synthetic conversation history with interleaved user/assistant
|
|
117
|
+
* messages and skill_load tool-use markers for the given skill IDs.
|
|
118
|
+
*
|
|
119
|
+
* Skill activations are spread evenly across the history.
|
|
120
|
+
*/
|
|
121
|
+
function buildHistory(messageCount: number, skillIds: string[]): Message[] {
|
|
122
|
+
const msgs: Message[] = [];
|
|
123
|
+
const activationInterval = Math.max(1, Math.floor(messageCount / skillIds.length));
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < messageCount; i++) {
|
|
126
|
+
// Every other message is user/assistant
|
|
127
|
+
if (i % 2 === 0) {
|
|
128
|
+
msgs.push({
|
|
129
|
+
role: 'user',
|
|
130
|
+
content: [
|
|
131
|
+
{ type: 'text', text: `User message ${i} about project tasks.` },
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
const blocks: Message['content'] = [
|
|
136
|
+
{ type: 'text', text: `Assistant response ${i} with analysis.` },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// Inject a skill_load tool_use at the activation point
|
|
140
|
+
const skillIndex = Math.floor(i / activationInterval);
|
|
141
|
+
if (skillIndex < skillIds.length) {
|
|
142
|
+
const skillId = skillIds[skillIndex];
|
|
143
|
+
const toolUseId = `tu-${skillId}-${i}`;
|
|
144
|
+
blocks.push({
|
|
145
|
+
type: 'tool_use',
|
|
146
|
+
id: toolUseId,
|
|
147
|
+
name: 'skill_load',
|
|
148
|
+
input: { skill_id: skillId },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
msgs.push({ role: 'assistant', content: blocks });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add matching tool_result messages for each skill_load tool_use
|
|
157
|
+
for (const msg of [...msgs]) {
|
|
158
|
+
for (const block of msg.content) {
|
|
159
|
+
if (block.type === 'tool_use' && block.name === 'skill_load') {
|
|
160
|
+
const skillId = (block.input as Record<string, string>).skill_id;
|
|
161
|
+
msgs.push({
|
|
162
|
+
role: 'user',
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: 'tool_result',
|
|
166
|
+
tool_use_id: block.id,
|
|
167
|
+
content: `<loaded_skill id="${skillId}" version="v1:deadbeef" />`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return msgs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function timeMs(fn: () => void): number {
|
|
179
|
+
const start = performance.now();
|
|
180
|
+
fn();
|
|
181
|
+
return performance.now() - start;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Benchmarks
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe('Skill projection benchmark', () => {
|
|
189
|
+
test('cold projection: 100 messages / 3 skills < 50ms', () => {
|
|
190
|
+
const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
|
|
191
|
+
catalogSkillIds = skillIds;
|
|
192
|
+
const history = buildHistory(100, skillIds);
|
|
193
|
+
|
|
194
|
+
const elapsed = timeMs(() => {
|
|
195
|
+
const result = projectSkillTools(history);
|
|
196
|
+
expect(result.toolDefinitions.length).toBeGreaterThan(0);
|
|
197
|
+
expect(result.allowedToolNames.size).toBeGreaterThan(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
console.log(` Cold projection (100 msgs / 3 skills): ${elapsed.toFixed(2)}ms`);
|
|
201
|
+
expect(elapsed).toBeLessThan(50);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('cached projection (no change) < 10ms', () => {
|
|
205
|
+
const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
|
|
206
|
+
catalogSkillIds = skillIds;
|
|
207
|
+
const history = buildHistory(100, skillIds);
|
|
208
|
+
const cache: SkillProjectionCache = {};
|
|
209
|
+
const prevActive = new Map<string, string>();
|
|
210
|
+
|
|
211
|
+
// Warm the cache
|
|
212
|
+
const warmResult = projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
|
|
213
|
+
|
|
214
|
+
// Snapshot cache object references after warm-up
|
|
215
|
+
const derivedAfterWarm = cache.derived!;
|
|
216
|
+
const entriesAfterWarm = cache.derived!.entries;
|
|
217
|
+
const seenIdsAfterWarm = cache.derived!.seenIds;
|
|
218
|
+
|
|
219
|
+
// Second call with identical history — should hit cache fast path
|
|
220
|
+
let cachedResult: ReturnType<typeof projectSkillTools> | undefined;
|
|
221
|
+
const elapsed = timeMs(() => {
|
|
222
|
+
cachedResult = projectSkillTools(history, {
|
|
223
|
+
cache,
|
|
224
|
+
previouslyActiveSkillIds: prevActive,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Assert cache was populated and covers all messages
|
|
229
|
+
expect(cache.derived).toBeDefined();
|
|
230
|
+
expect(cache.derived!.messageCount).toBe(history.length);
|
|
231
|
+
|
|
232
|
+
// Assert the cache object is reused (same reference, not rebuilt)
|
|
233
|
+
expect(cache.derived).toBe(derivedAfterWarm);
|
|
234
|
+
expect(cache.derived!.entries).toBe(entriesAfterWarm);
|
|
235
|
+
expect(cache.derived!.seenIds).toBe(seenIdsAfterWarm);
|
|
236
|
+
|
|
237
|
+
// Assert tool definitions are identical between warm and cached calls
|
|
238
|
+
expect(cachedResult!.toolDefinitions.length).toBe(warmResult.toolDefinitions.length);
|
|
239
|
+
const warmNames = warmResult.toolDefinitions.map((t) => t.name).sort();
|
|
240
|
+
const cachedNames = cachedResult!.toolDefinitions.map((t) => t.name).sort();
|
|
241
|
+
expect(cachedNames).toEqual(warmNames);
|
|
242
|
+
|
|
243
|
+
console.log(` Cached projection (no change): ${elapsed.toFixed(2)}ms`);
|
|
244
|
+
expect(elapsed).toBeLessThan(10);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('cache hit rate is 100% when history unchanged', () => {
|
|
248
|
+
const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
|
|
249
|
+
catalogSkillIds = skillIds;
|
|
250
|
+
const history = buildHistory(100, skillIds);
|
|
251
|
+
const cache: SkillProjectionCache = {};
|
|
252
|
+
const prevActive = new Map<string, string>();
|
|
253
|
+
|
|
254
|
+
// First call populates the cache
|
|
255
|
+
const firstResult = projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
|
|
256
|
+
expect(cache.derived).toBeDefined();
|
|
257
|
+
const snapshotDerived = cache.derived!;
|
|
258
|
+
const snapshotEntries = cache.derived!.entries;
|
|
259
|
+
const snapshotSeenIds = cache.derived!.seenIds;
|
|
260
|
+
|
|
261
|
+
// Run multiple subsequent calls with unchanged history
|
|
262
|
+
for (let i = 0; i < 5; i++) {
|
|
263
|
+
const result = projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
|
|
264
|
+
|
|
265
|
+
// Cache objects must be the same references (reused, not rebuilt)
|
|
266
|
+
expect(cache.derived).toBe(snapshotDerived);
|
|
267
|
+
expect(cache.derived!.entries).toBe(snapshotEntries);
|
|
268
|
+
expect(cache.derived!.seenIds).toBe(snapshotSeenIds);
|
|
269
|
+
|
|
270
|
+
// Tool definitions must match the first call exactly
|
|
271
|
+
expect(result.toolDefinitions.length).toBe(firstResult.toolDefinitions.length);
|
|
272
|
+
expect(result.toolDefinitions.map((t) => t.name).sort()).toEqual(
|
|
273
|
+
firstResult.toolDefinitions.map((t) => t.name).sort(),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('cold projection: 1000 messages / 5 skills < 100ms', () => {
|
|
279
|
+
const skillIds = [
|
|
280
|
+
'skill-alpha',
|
|
281
|
+
'skill-beta',
|
|
282
|
+
'skill-gamma',
|
|
283
|
+
'skill-delta',
|
|
284
|
+
'skill-epsilon',
|
|
285
|
+
];
|
|
286
|
+
catalogSkillIds = skillIds;
|
|
287
|
+
const history = buildHistory(1000, skillIds);
|
|
288
|
+
|
|
289
|
+
const elapsed = timeMs(() => {
|
|
290
|
+
const result = projectSkillTools(history);
|
|
291
|
+
expect(result.toolDefinitions.length).toBeGreaterThan(0);
|
|
292
|
+
expect(result.allowedToolNames.size).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
console.log(` Cold projection (1000 msgs / 5 skills): ${elapsed.toFixed(2)}ms`);
|
|
296
|
+
expect(elapsed).toBeLessThan(100);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('incremental scan (10 new messages appended) < 20ms', () => {
|
|
300
|
+
const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
|
|
301
|
+
catalogSkillIds = skillIds;
|
|
302
|
+
const history = buildHistory(100, skillIds);
|
|
303
|
+
const cache: SkillProjectionCache = {};
|
|
304
|
+
const prevActive = new Map<string, string>();
|
|
305
|
+
|
|
306
|
+
// Warm the cache
|
|
307
|
+
projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
|
|
308
|
+
|
|
309
|
+
// Append 10 new plain messages (no new skill activations)
|
|
310
|
+
for (let i = 0; i < 10; i++) {
|
|
311
|
+
history.push({
|
|
312
|
+
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
313
|
+
content: [{ type: 'text', text: `Follow-up message ${i}.` }],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const elapsed = timeMs(() => {
|
|
318
|
+
const result = projectSkillTools(history, {
|
|
319
|
+
cache,
|
|
320
|
+
previouslyActiveSkillIds: prevActive,
|
|
321
|
+
});
|
|
322
|
+
expect(result.toolDefinitions.length).toBeGreaterThan(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
console.log(` Incremental scan (10 new msgs): ${elapsed.toFixed(2)}ms`);
|
|
326
|
+
expect(elapsed).toBeLessThan(20);
|
|
327
|
+
});
|
|
328
|
+
});
|