@vellumai/assistant 0.4.49 → 0.4.50
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -55,6 +55,7 @@ mock.module("../../memory/embedding-local.js", () => ({
|
|
|
55
55
|
mock.module("../../memory/qdrant-client.js", () => ({
|
|
56
56
|
getQdrantClient: () => ({
|
|
57
57
|
searchWithFilter: async () => [],
|
|
58
|
+
hybridSearch: async () => [],
|
|
58
59
|
upsertPoints: async () => {},
|
|
59
60
|
deletePoints: async () => {},
|
|
60
61
|
}),
|
|
@@ -93,14 +94,14 @@ import {
|
|
|
93
94
|
memoryItemSources,
|
|
94
95
|
messages,
|
|
95
96
|
} from "../../memory/schema.js";
|
|
96
|
-
import {
|
|
97
|
+
import type { MemoryRecallToolResult } from "./handlers.js";
|
|
98
|
+
import { handleMemoryRecall } from "./handlers.js";
|
|
97
99
|
|
|
98
100
|
function clearTables() {
|
|
99
101
|
const db = getDb();
|
|
100
102
|
db.run("DELETE FROM memory_item_sources");
|
|
101
103
|
db.run("DELETE FROM memory_items");
|
|
102
104
|
db.run("DELETE FROM memory_segments");
|
|
103
|
-
db.run("DELETE FROM memory_segment_fts");
|
|
104
105
|
db.run("DELETE FROM messages");
|
|
105
106
|
db.run("DELETE FROM conversations");
|
|
106
107
|
}
|
|
@@ -237,7 +238,7 @@ function seedMemory() {
|
|
|
237
238
|
|
|
238
239
|
insertItem(db, {
|
|
239
240
|
id: "item-testing",
|
|
240
|
-
kind: "
|
|
241
|
+
kind: "identity",
|
|
241
242
|
subject: "testing",
|
|
242
243
|
statement: "The project uses bun test for unit testing",
|
|
243
244
|
firstSeenAt: now - 20_000,
|
|
@@ -284,7 +285,7 @@ describe("handleMemoryRecall", () => {
|
|
|
284
285
|
|
|
285
286
|
// ── Happy path ────────────────────────────────────────────────────
|
|
286
287
|
|
|
287
|
-
test("returns
|
|
288
|
+
test("returns valid result shape with Qdrant mocked empty", async () => {
|
|
288
289
|
seedMemory();
|
|
289
290
|
|
|
290
291
|
const result = await handleMemoryRecall(
|
|
@@ -292,35 +293,13 @@ describe("handleMemoryRecall", () => {
|
|
|
292
293
|
TEST_CONFIG,
|
|
293
294
|
);
|
|
294
295
|
|
|
296
|
+
// With Qdrant mocked empty, hybrid search returns nothing.
|
|
297
|
+
// Recency search also returns nothing (no conversationId passed to handler).
|
|
298
|
+
// The handler should return a valid result shape with zero results.
|
|
295
299
|
expect(result.isError).toBe(false);
|
|
296
300
|
const parsed = parseResult(result.content);
|
|
297
|
-
expect(parsed.resultCount).
|
|
298
|
-
expect(parsed.text
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("respects max_results parameter", async () => {
|
|
302
|
-
seedMemory();
|
|
303
|
-
|
|
304
|
-
const result = await handleMemoryRecall(
|
|
305
|
-
{ query: "API design", max_results: 1 },
|
|
306
|
-
TEST_CONFIG,
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
expect(result.isError).toBe(false);
|
|
310
|
-
const parsed = parseResult(result.content);
|
|
311
|
-
expect(parsed.resultCount).toBeLessThanOrEqual(1);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test("clamps max_results to 50", async () => {
|
|
315
|
-
seedMemory();
|
|
316
|
-
|
|
317
|
-
// Should not throw, max_results capped at 50
|
|
318
|
-
const result = await handleMemoryRecall(
|
|
319
|
-
{ query: "API design", max_results: 100 },
|
|
320
|
-
TEST_CONFIG,
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
expect(result.isError).toBe(false);
|
|
301
|
+
expect(typeof parsed.resultCount).toBe("number");
|
|
302
|
+
expect(typeof parsed.text).toBe("string");
|
|
324
303
|
});
|
|
325
304
|
|
|
326
305
|
// ── Empty results ─────────────────────────────────────────────────
|
|
@@ -336,10 +315,8 @@ describe("handleMemoryRecall", () => {
|
|
|
336
315
|
const parsed = parseResult(result.content);
|
|
337
316
|
expect(parsed.resultCount).toBe(0);
|
|
338
317
|
expect(parsed.text).toBe("No matching memories found.");
|
|
339
|
-
expect(parsed.sources.lexical).toBe(0);
|
|
340
318
|
expect(parsed.sources.semantic).toBe(0);
|
|
341
319
|
expect(parsed.sources.recency).toBe(0);
|
|
342
|
-
expect(parsed.sources.entity).toBe(0);
|
|
343
320
|
});
|
|
344
321
|
|
|
345
322
|
// ── Degraded mode ─────────────────────────────────────────────────
|
|
@@ -399,11 +376,13 @@ describe("handleMemoryRecall", () => {
|
|
|
399
376
|
// Not degraded because embeddings are optional
|
|
400
377
|
expect(parsed.degraded).toBe(false);
|
|
401
378
|
expect(parsed.sources.semantic).toBe(0);
|
|
402
|
-
//
|
|
403
|
-
|
|
379
|
+
// With FTS/direct-item search removed, only Qdrant hybrid search and
|
|
380
|
+
// recency search remain. Both return empty here (Qdrant mocked,
|
|
381
|
+
// no conversationId passed). The handler returns a valid empty result.
|
|
382
|
+
expect(parsed.resultCount).toBe(0);
|
|
404
383
|
});
|
|
405
384
|
|
|
406
|
-
test("returns
|
|
385
|
+
test("gracefully returns empty in degraded mode without embeddings", async () => {
|
|
407
386
|
seedMemory();
|
|
408
387
|
|
|
409
388
|
const degradedConfig: AssistantConfig = {
|
|
@@ -425,70 +404,76 @@ describe("handleMemoryRecall", () => {
|
|
|
425
404
|
|
|
426
405
|
expect(result.isError).toBe(false);
|
|
427
406
|
const parsed = parseResult(result.content);
|
|
428
|
-
//
|
|
429
|
-
|
|
407
|
+
// With FTS removed and Qdrant mocked, no retrieval path finds items.
|
|
408
|
+
// The handler returns a valid empty result without throwing.
|
|
409
|
+
expect(typeof parsed.resultCount).toBe("number");
|
|
430
410
|
});
|
|
431
411
|
|
|
432
412
|
// ── Scope filtering ───────────────────────────────────────────────
|
|
433
413
|
|
|
434
|
-
test("scope 'conversation'
|
|
414
|
+
test("scope 'conversation' passes scope policy override to retriever", async () => {
|
|
415
|
+
// Seed a conversation with segments in the target scope and in a different
|
|
416
|
+
// scope. With scope="conversation", only the target scope's segments should
|
|
417
|
+
// be returned (fallbackToDefault=false).
|
|
435
418
|
const db = getDb();
|
|
436
419
|
const now = Date.now();
|
|
420
|
+
const convId = "conv-scope-a";
|
|
421
|
+
|
|
422
|
+
insertConversation(db, convId, now - 10_000);
|
|
423
|
+
insertMessage(
|
|
424
|
+
db,
|
|
425
|
+
"msg-scope-a",
|
|
426
|
+
convId,
|
|
427
|
+
"user",
|
|
428
|
+
"scoped data for conversation A",
|
|
429
|
+
now - 5_000,
|
|
430
|
+
);
|
|
437
431
|
|
|
438
|
-
// Insert
|
|
439
|
-
|
|
440
|
-
id
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
432
|
+
// Insert a segment scoped to this conversation's scope
|
|
433
|
+
db.run(`
|
|
434
|
+
INSERT INTO memory_segments (id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at)
|
|
435
|
+
VALUES ('seg-scope-a', 'msg-scope-a', '${convId}', 'user', 0, 'Conversation-scoped data for conversation A', 8, '${convId}', ${
|
|
436
|
+
now - 5_000
|
|
437
|
+
}, ${now - 5_000})
|
|
438
|
+
`);
|
|
439
|
+
|
|
440
|
+
// Insert an out-of-scope segment that should NOT be returned
|
|
441
|
+
db.run(`
|
|
442
|
+
INSERT INTO memory_segments (id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at)
|
|
443
|
+
VALUES ('seg-scope-other', 'msg-scope-a', '${convId}', 'user', 1, 'Out-of-scope data from a different scope', 8, 'other-scope', ${
|
|
444
|
+
now - 5_000
|
|
445
|
+
}, ${now - 5_000})
|
|
446
|
+
`);
|
|
447
447
|
|
|
448
|
-
// Insert item in default scope
|
|
449
|
-
insertItem(db, {
|
|
450
|
-
id: "item-default",
|
|
451
|
-
kind: "fact",
|
|
452
|
-
subject: "default data",
|
|
453
|
-
statement: "This item is in the default scope about scoped data",
|
|
454
|
-
firstSeenAt: now - 10_000,
|
|
455
|
-
scopeId: "default",
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// Query with scope="conversation" and scopeId="conv-scope-a"
|
|
459
|
-
// should restrict to only that scope (no fallback to default)
|
|
460
448
|
const result = await handleMemoryRecall(
|
|
461
|
-
{ query: "
|
|
449
|
+
{ query: "data", scope: "conversation" },
|
|
462
450
|
TEST_CONFIG,
|
|
463
|
-
|
|
451
|
+
convId,
|
|
452
|
+
convId,
|
|
464
453
|
);
|
|
465
454
|
|
|
466
455
|
expect(result.isError).toBe(false);
|
|
467
456
|
const parsed = parseResult(result.content);
|
|
468
|
-
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
expect(parsed.
|
|
472
|
-
expect(parsed.text).toContain("
|
|
473
|
-
expect(parsed.text).not.toContain("default scope");
|
|
457
|
+
// With conversation scope, only the conversation-scoped segment is returned.
|
|
458
|
+
// The other-scope segment should be excluded (fallbackToDefault=false).
|
|
459
|
+
expect(parsed.sources.recency).toBe(1);
|
|
460
|
+
expect(parsed.text).toContain("Conversation-scoped data");
|
|
461
|
+
expect(parsed.text).not.toContain("Out-of-scope data");
|
|
474
462
|
});
|
|
475
463
|
|
|
476
|
-
test("default scope
|
|
464
|
+
test("default scope handler invocation does not error", async () => {
|
|
477
465
|
const db = getDb();
|
|
478
466
|
const now = Date.now();
|
|
479
467
|
|
|
480
|
-
// Insert item in default scope
|
|
481
468
|
insertItem(db, {
|
|
482
469
|
id: "item-fallback",
|
|
483
|
-
kind: "
|
|
470
|
+
kind: "identity",
|
|
484
471
|
subject: "global knowledge",
|
|
485
472
|
statement: "This global knowledge should be accessible from any scope",
|
|
486
473
|
firstSeenAt: now - 10_000,
|
|
487
474
|
scopeId: "default",
|
|
488
475
|
});
|
|
489
476
|
|
|
490
|
-
// Query with scope="default" (the default) and a specific scopeId
|
|
491
|
-
// should include fallback to default scope
|
|
492
477
|
const result = await handleMemoryRecall(
|
|
493
478
|
{ query: "global knowledge" },
|
|
494
479
|
TEST_CONFIG,
|
|
@@ -497,37 +482,36 @@ describe("handleMemoryRecall", () => {
|
|
|
497
482
|
|
|
498
483
|
expect(result.isError).toBe(false);
|
|
499
484
|
const parsed = parseResult(result.content);
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
expect(parsed.
|
|
485
|
+
// With Qdrant mocked and no conversation segments, the retriever returns
|
|
486
|
+
// empty. Handler should still return a valid result shape.
|
|
487
|
+
expect(typeof parsed.resultCount).toBe("number");
|
|
503
488
|
});
|
|
504
489
|
|
|
505
490
|
// ── Error handling ────────────────────────────────────────────────
|
|
506
491
|
|
|
507
492
|
test("retrieval failure returns error message, does not throw", async () => {
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
expect(typeof result.isError).toBe("boolean");
|
|
493
|
+
// Mock buildMemoryRecall to throw, simulating an internal retrieval failure
|
|
494
|
+
const retrieverModule = await import("../../memory/retriever.js");
|
|
495
|
+
const original = retrieverModule.buildMemoryRecall;
|
|
496
|
+
(retrieverModule as Record<string, unknown>).buildMemoryRecall =
|
|
497
|
+
async () => {
|
|
498
|
+
throw new Error("Simulated retrieval failure");
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const result = await handleMemoryRecall(
|
|
503
|
+
{ query: "test query" },
|
|
504
|
+
TEST_CONFIG,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// The handler should catch the error and return an error result,
|
|
508
|
+
// never throw
|
|
509
|
+
expect(result.isError).toBe(true);
|
|
510
|
+
expect(result.content).toContain("Simulated retrieval failure");
|
|
511
|
+
} finally {
|
|
512
|
+
// Restore original implementation
|
|
513
|
+
(retrieverModule as Record<string, unknown>).buildMemoryRecall = original;
|
|
514
|
+
}
|
|
531
515
|
});
|
|
532
516
|
|
|
533
517
|
test("result shape matches MemoryRecallToolResult when successful", async () => {
|
|
@@ -546,10 +530,8 @@ describe("handleMemoryRecall", () => {
|
|
|
546
530
|
expect(typeof parsed.resultCount).toBe("number");
|
|
547
531
|
expect(typeof parsed.degraded).toBe("boolean");
|
|
548
532
|
expect(typeof parsed.sources).toBe("object");
|
|
549
|
-
expect(typeof parsed.sources.lexical).toBe("number");
|
|
550
533
|
expect(typeof parsed.sources.semantic).toBe("number");
|
|
551
534
|
expect(typeof parsed.sources.recency).toBe("number");
|
|
552
|
-
expect(typeof parsed.sources.entity).toBe("number");
|
|
553
535
|
});
|
|
554
536
|
|
|
555
537
|
test("empty result shape matches MemoryRecallToolResult", async () => {
|
|
@@ -564,9 +546,7 @@ describe("handleMemoryRecall", () => {
|
|
|
564
546
|
expect(parsed.text).toBe("No matching memories found.");
|
|
565
547
|
expect(parsed.resultCount).toBe(0);
|
|
566
548
|
expect(typeof parsed.degraded).toBe("boolean");
|
|
567
|
-
expect(parsed.sources.lexical).toBe(0);
|
|
568
549
|
expect(parsed.sources.semantic).toBe(0);
|
|
569
550
|
expect(parsed.sources.recency).toBe(0);
|
|
570
|
-
expect(parsed.sources.entity).toBe(0);
|
|
571
551
|
});
|
|
572
552
|
});
|
|
@@ -3,17 +3,9 @@ import { v4 as uuid } from "uuid";
|
|
|
3
3
|
|
|
4
4
|
import type { AssistantConfig } from "../../config/types.js";
|
|
5
5
|
import { getDb } from "../../memory/db.js";
|
|
6
|
-
import {
|
|
7
|
-
getMemoryBackendStatus,
|
|
8
|
-
logMemoryEmbeddingWarning,
|
|
9
|
-
} from "../../memory/embedding-backend.js";
|
|
10
6
|
import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
|
|
11
|
-
import { formatRecallText } from "../../memory/format-recall.js";
|
|
12
7
|
import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
13
|
-
import {
|
|
14
|
-
collectAndMergeCandidates,
|
|
15
|
-
embedWithRetry,
|
|
16
|
-
} from "../../memory/retriever.js";
|
|
8
|
+
import { buildMemoryRecall } from "../../memory/retriever.js";
|
|
17
9
|
import { memoryItems } from "../../memory/schema.js";
|
|
18
10
|
import type { ScopePolicyOverride } from "../../memory/search/types.js";
|
|
19
11
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -39,21 +31,37 @@ export async function handleMemorySave(
|
|
|
39
31
|
};
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
const
|
|
34
|
+
const rawKind = args.kind;
|
|
43
35
|
const validKinds = new Set([
|
|
36
|
+
"identity",
|
|
44
37
|
"preference",
|
|
45
|
-
"
|
|
38
|
+
"project",
|
|
46
39
|
"decision",
|
|
47
|
-
"
|
|
48
|
-
"relationship",
|
|
40
|
+
"constraint",
|
|
49
41
|
"event",
|
|
50
|
-
"opinion",
|
|
51
|
-
"instruction",
|
|
52
|
-
"style",
|
|
53
|
-
"playbook",
|
|
54
|
-
"learning",
|
|
55
42
|
]);
|
|
56
|
-
|
|
43
|
+
/** Maps old kind names to their new equivalents for backwards compat. */
|
|
44
|
+
const kindMigrationMap: Record<string, string> = {
|
|
45
|
+
profile: "identity",
|
|
46
|
+
fact: "identity",
|
|
47
|
+
relationship: "identity",
|
|
48
|
+
opinion: "preference",
|
|
49
|
+
todo: "project",
|
|
50
|
+
instruction: "constraint",
|
|
51
|
+
style: "preference",
|
|
52
|
+
playbook: "constraint",
|
|
53
|
+
learning: "identity",
|
|
54
|
+
};
|
|
55
|
+
if (typeof rawKind !== "string") {
|
|
56
|
+
return {
|
|
57
|
+
content: `Error: kind is required and must be one of: ${[
|
|
58
|
+
...validKinds,
|
|
59
|
+
].join(", ")}`,
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const kind = kindMigrationMap[rawKind] ?? rawKind;
|
|
64
|
+
if (!validKinds.has(kind)) {
|
|
57
65
|
return {
|
|
58
66
|
content: `Error: kind is required and must be one of: ${[
|
|
59
67
|
...validKinds,
|
|
@@ -255,10 +263,8 @@ export interface MemoryRecallToolResult {
|
|
|
255
263
|
degraded: boolean;
|
|
256
264
|
items: Array<{ id: string; type: string; kind: string }>;
|
|
257
265
|
sources: {
|
|
258
|
-
lexical: number;
|
|
259
266
|
semantic: number;
|
|
260
267
|
recency: number;
|
|
261
|
-
entity: number;
|
|
262
268
|
};
|
|
263
269
|
}
|
|
264
270
|
|
|
@@ -276,11 +282,6 @@ export async function handleMemoryRecall(
|
|
|
276
282
|
};
|
|
277
283
|
}
|
|
278
284
|
|
|
279
|
-
const maxResults =
|
|
280
|
-
typeof args.max_results === "number" && args.max_results > 0
|
|
281
|
-
? Math.min(args.max_results, 50)
|
|
282
|
-
: 10;
|
|
283
|
-
|
|
284
285
|
const scope =
|
|
285
286
|
typeof args.scope === "string" && args.scope.trim().length > 0
|
|
286
287
|
? args.scope.trim()
|
|
@@ -298,54 +299,28 @@ export async function handleMemoryRecall(
|
|
|
298
299
|
try {
|
|
299
300
|
const trimmedQuery = query.trim();
|
|
300
301
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
queryVector = embedded.vectors[0] ?? null;
|
|
312
|
-
provider = embedded.provider;
|
|
313
|
-
model = embedded.model;
|
|
314
|
-
} catch (err) {
|
|
315
|
-
logMemoryEmbeddingWarning(err, "query");
|
|
316
|
-
degraded = !!config.memory.embeddings.required;
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
degraded = backendStatus.degraded;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Run the full retrieval pipeline with all sources enabled
|
|
323
|
-
const collected = await collectAndMergeCandidates(trimmedQuery, config, {
|
|
324
|
-
queryVector,
|
|
325
|
-
provider,
|
|
326
|
-
model,
|
|
327
|
-
conversationId,
|
|
328
|
-
scopeId,
|
|
329
|
-
scopePolicyOverride,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
if (collected.semanticSearchFailed || collected.semanticUnavailable) {
|
|
333
|
-
degraded = true;
|
|
334
|
-
}
|
|
302
|
+
// Use the unified recall pipeline
|
|
303
|
+
const recall = await buildMemoryRecall(
|
|
304
|
+
trimmedQuery,
|
|
305
|
+
conversationId ?? "",
|
|
306
|
+
config,
|
|
307
|
+
{
|
|
308
|
+
scopeId,
|
|
309
|
+
scopePolicyOverride,
|
|
310
|
+
},
|
|
311
|
+
);
|
|
335
312
|
|
|
336
|
-
const
|
|
313
|
+
const degraded = recall.degraded;
|
|
337
314
|
|
|
338
|
-
if (
|
|
315
|
+
if (recall.selectedCount === 0 || recall.injectedText.length === 0) {
|
|
339
316
|
const result: MemoryRecallToolResult = {
|
|
340
317
|
text: "No matching memories found.",
|
|
341
318
|
resultCount: 0,
|
|
342
319
|
degraded,
|
|
343
320
|
items: [],
|
|
344
321
|
sources: {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
recency: 0,
|
|
348
|
-
entity: 0,
|
|
322
|
+
semantic: recall.semanticHits,
|
|
323
|
+
recency: recall.recencyHits,
|
|
349
324
|
},
|
|
350
325
|
};
|
|
351
326
|
return {
|
|
@@ -354,28 +329,18 @@ export async function handleMemoryRecall(
|
|
|
354
329
|
};
|
|
355
330
|
}
|
|
356
331
|
|
|
357
|
-
// Format candidates into readable text using the shared formatter
|
|
358
|
-
const formatted = formatRecallText(candidates, {
|
|
359
|
-
format: config.memory.retrieval.injectionFormat,
|
|
360
|
-
maxTokens: config.memory.retrieval.maxInjectTokens,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const items = formatted.selected.map((c) => ({
|
|
364
|
-
id: c.id,
|
|
365
|
-
type: c.type,
|
|
366
|
-
kind: c.kind,
|
|
367
|
-
}));
|
|
368
|
-
|
|
369
332
|
const result: MemoryRecallToolResult = {
|
|
370
|
-
text:
|
|
371
|
-
resultCount:
|
|
333
|
+
text: recall.injectedText,
|
|
334
|
+
resultCount: recall.selectedCount,
|
|
372
335
|
degraded,
|
|
373
|
-
items
|
|
336
|
+
items: recall.topCandidates.map((c) => ({
|
|
337
|
+
id: c.key,
|
|
338
|
+
type: c.type,
|
|
339
|
+
kind: c.kind,
|
|
340
|
+
})),
|
|
374
341
|
sources: {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
recency: collected.recency.length,
|
|
378
|
-
entity: collected.entity.length,
|
|
342
|
+
semantic: recall.semanticHits,
|
|
343
|
+
recency: recall.recencyHits,
|
|
379
344
|
},
|
|
380
345
|
};
|
|
381
346
|
|
|
@@ -22,8 +22,15 @@ const VALID_ROUTING_INTENTS: RoutingIntent[] = [
|
|
|
22
22
|
|
|
23
23
|
export async function executeScheduleCreate(
|
|
24
24
|
input: Record<string, unknown>,
|
|
25
|
-
|
|
25
|
+
context: ToolContext,
|
|
26
26
|
): Promise<ToolExecutionResult> {
|
|
27
|
+
if (context.trustClass !== "guardian") {
|
|
28
|
+
return {
|
|
29
|
+
content:
|
|
30
|
+
"Error: schedule_create is restricted to guardian actors because schedules execute with elevated privileges.",
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
27
34
|
const name = input.name as string;
|
|
28
35
|
const timezone = (input.timezone as string) ?? null;
|
|
29
36
|
const message = input.message as string;
|
|
@@ -25,8 +25,15 @@ const VALID_ROUTING_INTENTS: RoutingIntent[] = [
|
|
|
25
25
|
|
|
26
26
|
export async function executeScheduleUpdate(
|
|
27
27
|
input: Record<string, unknown>,
|
|
28
|
-
|
|
28
|
+
context: ToolContext,
|
|
29
29
|
): Promise<ToolExecutionResult> {
|
|
30
|
+
if (context.trustClass !== "guardian") {
|
|
31
|
+
return {
|
|
32
|
+
content:
|
|
33
|
+
"Error: schedule_update is restricted to guardian actors because schedules execute with elevated privileges.",
|
|
34
|
+
isError: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
30
37
|
const jobId = input.job_id as string;
|
|
31
38
|
if (!jobId || typeof jobId !== "string") {
|
|
32
39
|
return { content: "Error: job_id is required", isError: true };
|
package/src/tools/skills/load.ts
CHANGED
|
@@ -8,7 +8,10 @@ import type { SkillSummary, SkillToolManifest } from "../../config/skills.js";
|
|
|
8
8
|
import { loadSkillBySelector, loadSkillCatalog } from "../../config/skills.js";
|
|
9
9
|
import { RiskLevel } from "../../permissions/types.js";
|
|
10
10
|
import type { ToolDefinition } from "../../providers/types.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
autoInstallFromCatalog,
|
|
13
|
+
resolveCatalog,
|
|
14
|
+
} from "../../skills/catalog-install.js";
|
|
12
15
|
import {
|
|
13
16
|
collectAllMissing,
|
|
14
17
|
indexCatalogById,
|
|
@@ -191,15 +194,35 @@ export class SkillLoadTool implements Tool {
|
|
|
191
194
|
catalogIndex = indexCatalogById(catalog);
|
|
192
195
|
|
|
193
196
|
// Auto-install missing includes before validation (max 5 rounds for transitive deps)
|
|
197
|
+
// Defer catalog resolution until we confirm there are missing includes,
|
|
198
|
+
// then cache the result to avoid redundant network requests per dependency.
|
|
199
|
+
let remoteCatalog: Awaited<ReturnType<typeof resolveCatalog>> | undefined;
|
|
200
|
+
|
|
194
201
|
const MAX_INSTALL_ROUNDS = 5;
|
|
195
202
|
for (let round = 0; round < MAX_INSTALL_ROUNDS; round++) {
|
|
196
203
|
const missing = collectAllMissing(skill.id, catalogIndex);
|
|
197
204
|
if (missing.size === 0) break;
|
|
198
205
|
|
|
206
|
+
// Lazily resolve catalog on first round with missing includes
|
|
207
|
+
if (!remoteCatalog) {
|
|
208
|
+
try {
|
|
209
|
+
remoteCatalog = await resolveCatalog([...missing][0]);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
log.warn(
|
|
212
|
+
{ err, skillId: skill.id },
|
|
213
|
+
"Failed to resolve catalog for include auto-install",
|
|
214
|
+
);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
let installedAny = false;
|
|
200
220
|
for (const missingId of missing) {
|
|
201
221
|
try {
|
|
202
|
-
const installed = await autoInstallFromCatalog(
|
|
222
|
+
const installed = await autoInstallFromCatalog(
|
|
223
|
+
missingId,
|
|
224
|
+
remoteCatalog,
|
|
225
|
+
);
|
|
203
226
|
if (installed) {
|
|
204
227
|
log.info(
|
|
205
228
|
{ skillId: missingId, parentSkillId: skill.id },
|