@botbotgo/agent-harness 0.0.154 → 0.0.155

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.
@@ -21,16 +21,17 @@ import { cancelRunOperation, executeQueuedRunOperation, resumeRun } from "./harn
21
21
  import { acquireRunSlot as acquireHarnessRunSlot } from "./harness/run/run-slot-acquisition.js";
22
22
  import { dropPendingRunSlot, enqueuePendingRunSlot } from "./harness/run/run-queue.js";
23
23
  import { getDefaultRuntimeEntryAgentId, resolveSelectedAgentId, routeAgentId } from "./harness/run/routing.js";
24
- import { resolveStoreFromConfig, } from "./harness/run/resources.js";
24
+ import { resolveStoreFromConfig, resolveVectorStore, } from "./harness/run/resources.js";
25
25
  import { createToolMcpServerFromTools, serveToolsOverStdioFromHarness } from "../mcp.js";
26
26
  import { closeMcpClientsForWorkspace } from "../resource/mcp-tool-support.js";
27
27
  import { getBindingRuntimeExecutionMode, } from "./support/compiled-binding.js";
28
28
  import { bindingSupportsRunningReplay, getWorkspaceBinding, resolveWorkspaceAgentTools, } from "./harness/bindings.js";
29
29
  import { describeWorkspaceInventory, getAgentInventoryRecord, listAgentSkills as listWorkspaceAgentSkills, } from "./harness/system/inventory.js";
30
30
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
31
- import { Mem0IngestionSync, readMem0RuntimeConfig } from "./harness/system/mem0-ingestion-sync.js";
31
+ import { Mem0IngestionSync, Mem0SemanticRecall, readMem0RuntimeConfig, } from "./harness/system/mem0-ingestion-sync.js";
32
+ import { createRuntimeMemoryManager, RuntimeMemoryFormationSync, readRuntimeMemoryFormationConfig, } from "./harness/system/runtime-memory-manager.js";
32
33
  import { renderMemoryCandidatesMarkdown } from "./harness/system/runtime-memory-candidates.js";
33
- import { listMemoryRecordsForScopes, persistStructuredMemoryRecords, } from "./harness/system/runtime-memory-records.js";
34
+ import { getMemoryRecord, listMemoryRecordsForScopes, persistStructuredMemoryRecords, } from "./harness/system/runtime-memory-records.js";
34
35
  import { consolidateStructuredMemoryScope } from "./harness/system/runtime-memory-consolidation.js";
35
36
  import { normalizeLangMemMemoryKind, readRuntimeMemoryMaintenanceConfig, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, scoreMemoryText, } from "./harness/system/runtime-memory-policy.js";
36
37
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
@@ -70,6 +71,11 @@ export class AgentHarnessRuntime {
70
71
  unregisterRuntimeMemorySync;
71
72
  mem0IngestionSync;
72
73
  unregisterMem0IngestionSync;
74
+ mem0SemanticRecall;
75
+ runtimeMemoryFormationConfig;
76
+ runtimeMemoryManager;
77
+ runtimeMemoryFormationSync;
78
+ unregisterRuntimeMemoryFormationSync;
73
79
  resolvedRuntimeAdapterOptions;
74
80
  healthMonitor;
75
81
  recoveryConfig;
@@ -84,6 +90,19 @@ export class AgentHarnessRuntime {
84
90
  closed = false;
85
91
  backgroundEventRuntime;
86
92
  runtimeEventOperations;
93
+ async resolveRuntimeMemoryVectorStore() {
94
+ const vectorStoreConfig = typeof this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory?.vectorStore === "object" &&
95
+ this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory?.vectorStore
96
+ ? this.defaultRuntimeEntryBinding.harnessRuntime.runtimeMemory.vectorStore
97
+ : undefined;
98
+ const vectorStoreRef = typeof vectorStoreConfig?.ref === "string" && vectorStoreConfig.ref.trim().length > 0 ? vectorStoreConfig.ref : undefined;
99
+ try {
100
+ return await resolveVectorStore(this.workspace, this.vectorStores, vectorStoreRef, this.resolvedRuntimeAdapterOptions);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
87
106
  defaultRunRoot() {
88
107
  return this.defaultRunRootValue;
89
108
  }
@@ -185,10 +204,40 @@ export class AgentHarnessRuntime {
185
204
  if (mem0RuntimeConfig) {
186
205
  this.mem0IngestionSync = new Mem0IngestionSync(this.persistence, mem0RuntimeConfig, runRoot);
187
206
  this.unregisterMem0IngestionSync = this.eventBus.registerProjection(this.mem0IngestionSync);
207
+ this.mem0SemanticRecall = new Mem0SemanticRecall(mem0RuntimeConfig);
188
208
  }
189
209
  else {
190
210
  this.mem0IngestionSync = null;
191
211
  this.unregisterMem0IngestionSync = () => { };
212
+ this.mem0SemanticRecall = null;
213
+ }
214
+ const runtimeMemoryFormationConfig = readRuntimeMemoryFormationConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory, this.workspace.workspaceRoot);
215
+ this.runtimeMemoryFormationConfig = runtimeMemoryFormationConfig ?? null;
216
+ this.runtimeMemoryManager =
217
+ this.defaultRuntimeEntryBinding && runtimeMemoryFormationConfig
218
+ ? createRuntimeMemoryManager({
219
+ workspace: this.workspace,
220
+ binding: this.defaultRuntimeEntryBinding,
221
+ config: runtimeMemoryFormationConfig,
222
+ modelResolver: this.resolvedRuntimeAdapterOptions.modelResolver,
223
+ })
224
+ : null;
225
+ if (runtimeMemoryFormationConfig) {
226
+ this.runtimeMemoryFormationSync = new RuntimeMemoryFormationSync(this.persistence, runtimeMemoryFormationConfig, (input) => this.persistStructuredMemoryCandidates(this.defaultRuntimeEntryBinding, {
227
+ candidates: input.candidates,
228
+ threadId: input.threadId,
229
+ runId: input.runId,
230
+ agentId: input.agentId,
231
+ userId: input.userId,
232
+ projectId: input.projectId,
233
+ recordedAt: input.recordedAt,
234
+ storeCandidateLog: false,
235
+ }).then(() => undefined), runRoot);
236
+ this.unregisterRuntimeMemoryFormationSync = this.eventBus.registerProjection(this.runtimeMemoryFormationSync);
237
+ }
238
+ else {
239
+ this.runtimeMemoryFormationSync = null;
240
+ this.unregisterRuntimeMemoryFormationSync = () => { };
192
241
  }
193
242
  this.recoveryConfig = getRecoveryConfig(workspace.refs);
194
243
  this.concurrencyConfig = getConcurrencyConfig(workspace.refs);
@@ -294,23 +343,20 @@ export class AgentHarnessRuntime {
294
343
  ? input.topK
295
344
  : (this.runtimeMemoryPolicy?.retrieval.defaultTopK ?? 5);
296
345
  const kinds = input.kinds?.length ? new Set(input.kinds) : null;
297
- const items = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, scopes))
298
- .filter((record) => this.matchesRecallScope(record, {
299
- threadId: input.threadId,
300
- agentId,
301
- workspaceId: input.workspaceId ?? workspaceId,
302
- userId,
303
- projectId,
304
- }))
305
- .filter((record) => (input.includeStale ? record.status === "active" || record.status === "stale" : record.status === "active"))
306
- .filter((record) => (kinds ? kinds.has(record.kind) : true))
307
- .map((record) => ({
308
- record,
309
- score: scoreMemoryText(input.query.trim(), `${record.summary}\n${record.content}`, this.getMemoryScopeBoost(record.scope)) +
310
- Math.max(0, 1 - ((Date.now() - Date.parse(record.lastConfirmedAt)) / (1000 * 60 * 60 * 24 * 365))) +
311
- record.confidence,
346
+ const items = (await this.rankRecallCandidates(binding, {
347
+ query: input.query.trim(),
348
+ scopes,
349
+ kinds,
350
+ topK,
351
+ includeStale: input.includeStale === true,
352
+ filters: {
353
+ threadId: input.threadId,
354
+ agentId,
355
+ workspaceId: input.workspaceId ?? workspaceId,
356
+ userId,
357
+ projectId,
358
+ },
312
359
  }))
313
- .sort((left, right) => right.score - left.score)
314
360
  .slice(0, topK)
315
361
  .map(({ record }) => record);
316
362
  return { items };
@@ -518,36 +564,236 @@ export class AgentHarnessRuntime {
518
564
  }
519
565
  return 0;
520
566
  }
521
- async buildRuntimeMemoryContext(binding, threadId, input) {
522
- const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
523
- const ranked = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace"]))
524
- .filter((record) => {
525
- if (record.status !== "active") {
526
- return false;
567
+ memoryFreshnessBoost(value) {
568
+ return Math.max(0, 1 - ((Date.now() - Date.parse(value)) / (1000 * 60 * 60 * 24 * 365)));
569
+ }
570
+ scoreStructuredRecord(query, record) {
571
+ return (scoreMemoryText(query, `${record.summary}\n${record.content}`, this.getMemoryScopeBoost(record.scope)) +
572
+ this.memoryFreshnessBoost(record.lastConfirmedAt) +
573
+ record.confidence);
574
+ }
575
+ normalizeMemoryDedupKey(record) {
576
+ return `${record.scope}::${record.summary.toLowerCase().replace(/\s+/g, " ").trim()}::${record.content.toLowerCase().replace(/\s+/g, " ").trim()}`;
577
+ }
578
+ inferMem0MemoryKind(hit) {
579
+ const candidates = [
580
+ ...hit.categories,
581
+ typeof hit.metadata.kind === "string" ? hit.metadata.kind : undefined,
582
+ typeof hit.metadata.memoryType === "string" ? hit.metadata.memoryType : undefined,
583
+ ].filter((value) => typeof value === "string" && value.trim().length > 0);
584
+ for (const candidate of candidates) {
585
+ const kind = normalizeLangMemMemoryKind(candidate);
586
+ if (kind === "semantic" || kind === "episodic" || kind === "procedural") {
587
+ return kind;
527
588
  }
528
- if (record.scope === "thread") {
529
- return String(record.provenance.threadId ?? "") === threadId;
589
+ }
590
+ return "semantic";
591
+ }
592
+ inferMem0MemoryScope(hit, filters) {
593
+ const metadataScope = typeof hit.metadata.scope === "string" ? hit.metadata.scope : undefined;
594
+ if (metadataScope === "thread" || metadataScope === "agent" || metadataScope === "workspace" || metadataScope === "user" || metadataScope === "project") {
595
+ return metadataScope;
596
+ }
597
+ if (typeof hit.metadata.threadId === "string" && filters.threadId && hit.metadata.threadId === filters.threadId) {
598
+ return "thread";
599
+ }
600
+ if (hit.agentId === filters.agentId || hit.metadata.agentId === filters.agentId) {
601
+ return "agent";
602
+ }
603
+ return "workspace";
604
+ }
605
+ createMem0MemoryRecord(hit, filters) {
606
+ const scope = this.inferMem0MemoryScope(hit, filters);
607
+ const kind = this.inferMem0MemoryKind(hit);
608
+ const summary = hit.memory.split("\n")[0]?.trim() || hit.memory;
609
+ return {
610
+ id: `mem0:${hit.id}`,
611
+ canonicalKey: `mem0:${hit.id}`,
612
+ kind,
613
+ scope,
614
+ content: hit.memory,
615
+ summary: summary.slice(0, 240),
616
+ status: "active",
617
+ confidence: Math.max(0, Math.min(1, hit.score || 0.5)),
618
+ createdAt: hit.createdAt,
619
+ observedAt: hit.createdAt,
620
+ lastConfirmedAt: hit.updatedAt,
621
+ sourceType: "mem0-search",
622
+ sourceRefs: [`mem0:${hit.id}`],
623
+ tags: hit.categories,
624
+ provenance: {
625
+ source: "mem0",
626
+ threadId: typeof hit.metadata.threadId === "string" ? hit.metadata.threadId : filters.threadId,
627
+ runId: hit.runId ?? (typeof hit.metadata.runId === "string" ? hit.metadata.runId : undefined),
628
+ agentId: hit.agentId ?? (typeof hit.metadata.agentId === "string" ? hit.metadata.agentId : filters.agentId),
629
+ workspaceId: filters.workspaceId,
630
+ userId: filters.userId,
631
+ projectId: filters.projectId,
632
+ ...hit.metadata,
633
+ },
634
+ revision: 1,
635
+ supersedes: [],
636
+ conflictsWith: [],
637
+ };
638
+ }
639
+ async rankRecallCandidates(binding, input) {
640
+ const structuredRecords = await listMemoryRecordsForScopes(this.runtimeMemoryStore, input.scopes);
641
+ const ranked = structuredRecords
642
+ .filter((record) => this.matchesRecallScope(record, input.filters))
643
+ .filter((record) => (input.includeStale ? record.status === "active" || record.status === "stale" : record.status === "active"))
644
+ .filter((record) => (input.kinds ? input.kinds.has(record.kind) : true))
645
+ .map((record) => ({
646
+ record,
647
+ score: this.scoreStructuredRecord(input.query, record),
648
+ }));
649
+ const deduped = new Map();
650
+ for (const item of ranked) {
651
+ deduped.set(this.normalizeMemoryDedupKey(item.record), item);
652
+ }
653
+ const vectorStore = await this.resolveRuntimeMemoryVectorStore();
654
+ if (vectorStore) {
655
+ try {
656
+ const vectorHits = await vectorStore.similaritySearch(input.query, Math.max(input.topK, this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? input.topK));
657
+ for (const hit of vectorHits) {
658
+ const metadata = typeof hit.metadata === "object" && hit.metadata && !Array.isArray(hit.metadata)
659
+ ? hit.metadata
660
+ : {};
661
+ const recordId = typeof metadata.recordId === "string" ? metadata.recordId : undefined;
662
+ const scope = metadata.scope;
663
+ if (!recordId || (scope !== "thread" && scope !== "agent" && scope !== "workspace" && scope !== "user" && scope !== "project")) {
664
+ continue;
665
+ }
666
+ const canonical = await getMemoryRecord(this.runtimeMemoryStore, scope, recordId);
667
+ if (!canonical) {
668
+ continue;
669
+ }
670
+ if (!this.matchesRecallScope(canonical, input.filters)) {
671
+ continue;
672
+ }
673
+ if (input.includeStale ? canonical.status !== "active" && canonical.status !== "stale" : canonical.status !== "active") {
674
+ continue;
675
+ }
676
+ if (input.kinds && !input.kinds.has(canonical.kind)) {
677
+ continue;
678
+ }
679
+ const key = this.normalizeMemoryDedupKey(canonical);
680
+ const score = this.scoreStructuredRecord(input.query, canonical) + (typeof hit.score === "number" ? hit.score : 0);
681
+ const existing = deduped.get(key);
682
+ if (!existing || score > existing.score) {
683
+ deduped.set(key, { record: canonical, score });
684
+ }
685
+ }
530
686
  }
531
- if (record.scope === "agent") {
532
- return String(record.provenance.agentId ?? "") === binding.agent.id;
687
+ catch {
688
+ // Fail open to lexical/runtime ranking.
533
689
  }
534
- return true;
535
- })
690
+ }
691
+ const supportsMem0Scope = input.scopes.some((scope) => scope === "thread" || scope === "agent" || scope === "workspace");
692
+ if (!this.mem0SemanticRecall || !supportsMem0Scope) {
693
+ return Array.from(deduped.values()).sort((left, right) => right.score - left.score);
694
+ }
695
+ try {
696
+ const hits = await this.mem0SemanticRecall.search({
697
+ query: input.query,
698
+ topK: Math.max(input.topK, this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? input.topK),
699
+ agentId: input.filters.agentId,
700
+ threadId: input.filters.threadId,
701
+ });
702
+ for (const hit of hits) {
703
+ const record = this.createMem0MemoryRecord(hit, input.filters);
704
+ if (!input.scopes.includes(record.scope)) {
705
+ continue;
706
+ }
707
+ if (input.kinds && !input.kinds.has(record.kind)) {
708
+ continue;
709
+ }
710
+ const key = this.normalizeMemoryDedupKey(record);
711
+ if (deduped.has(key)) {
712
+ continue;
713
+ }
714
+ deduped.set(key, {
715
+ record,
716
+ score: this.scoreStructuredRecord(input.query, record) + Math.max(0, Math.min(1, hit.score)) * 4,
717
+ });
718
+ }
719
+ return Array.from(deduped.values()).sort((left, right) => right.score - left.score);
720
+ }
721
+ catch {
722
+ return Array.from(deduped.values()).sort((left, right) => right.score - left.score);
723
+ }
724
+ }
725
+ async rebuildRuntimeMemoryVectorIndex() {
726
+ const vectorStore = await this.resolveRuntimeMemoryVectorStore();
727
+ if (!vectorStore) {
728
+ return;
729
+ }
730
+ const records = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace", "user", "project"]))
731
+ .filter((record) => record.status === "active");
732
+ try {
733
+ await vectorStore.delete({ deleteAll: true });
734
+ if (records.length === 0) {
735
+ return;
736
+ }
737
+ await vectorStore.addDocuments(records.map((record) => ({
738
+ pageContent: `${record.summary}\n${record.content}`,
739
+ metadata: {
740
+ recordId: record.id,
741
+ scope: record.scope,
742
+ kind: record.kind,
743
+ status: record.status,
744
+ threadId: record.provenance.threadId,
745
+ agentId: record.provenance.agentId,
746
+ workspaceId: record.provenance.workspaceId,
747
+ userId: record.provenance.userId,
748
+ projectId: record.provenance.projectId,
749
+ confidence: record.confidence,
750
+ lastConfirmedAt: record.lastConfirmedAt,
751
+ },
752
+ })));
753
+ }
754
+ catch {
755
+ // Fail open: vector indexing must not break durable memory writes.
756
+ }
757
+ }
758
+ async buildRuntimeMemoryContext(binding, threadId, input) {
759
+ const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
760
+ const workspaceId = this.getWorkspaceId(binding);
761
+ const ranked = (await this.rankRecallCandidates(binding, {
762
+ query,
763
+ scopes: ["thread", "agent", "workspace", "user", "project"],
764
+ kinds: null,
765
+ topK: this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8,
766
+ includeStale: false,
767
+ filters: {
768
+ threadId,
769
+ agentId: binding.agent.id,
770
+ workspaceId,
771
+ userId: "default",
772
+ projectId: workspaceId,
773
+ },
774
+ }))
536
775
  .map((record) => {
537
- const scopeBoost = record.scope === "thread" ? 4 : record.scope === "agent" ? 2 : 1;
538
- const freshnessBoost = Math.max(0, 1 - ((Date.now() - Date.parse(record.lastConfirmedAt)) / (1000 * 60 * 60 * 24 * 365)));
776
+ const scopeBoost = record.record.scope === "thread"
777
+ ? 4
778
+ : record.record.scope === "agent"
779
+ ? 3
780
+ : record.record.scope === "user"
781
+ ? 2
782
+ : 1;
539
783
  return {
540
784
  content: [
541
- `# Durable ${record.scope[0].toUpperCase()}${record.scope.slice(1)} Memory`,
785
+ `# Durable ${record.record.scope[0].toUpperCase()}${record.record.scope.slice(1)} Memory`,
542
786
  "",
543
- `- summary: ${record.summary}`,
544
- `- kind: ${record.kind}`,
545
- `- scope: ${record.scope}`,
546
- `- confidence: ${record.confidence.toFixed(2)}`,
787
+ `- summary: ${record.record.summary}`,
788
+ `- kind: ${record.record.kind}`,
789
+ `- scope: ${record.record.scope}`,
790
+ `- confidence: ${record.record.confidence.toFixed(2)}`,
547
791
  "",
548
- record.content,
792
+ record.record.content,
549
793
  ].join("\n"),
550
- score: scoreMemoryText(query, `${record.summary}\n${record.content}`, scopeBoost) + freshnessBoost + record.confidence,
794
+ score: scoreMemoryText(query, `${record.record.summary}\n${record.record.content}`, scopeBoost) +
795
+ this.memoryFreshnessBoost(record.record.lastConfirmedAt) +
796
+ record.record.confidence,
551
797
  };
552
798
  })
553
799
  .sort((left, right) => right.score - left.score)
@@ -561,6 +807,9 @@ export class AgentHarnessRuntime {
561
807
  if (!Array.isArray(value) || value.length === 0) {
562
808
  return;
563
809
  }
810
+ if (this.runtimeMemoryFormationConfig?.hotPath.enabled === false) {
811
+ return;
812
+ }
564
813
  const candidates = value
565
814
  .filter((candidate) => typeof candidate === "object" && candidate !== null)
566
815
  .filter((candidate) => candidate.noStore !== true && typeof candidate.content === "string" && candidate.content.trim().length > 0);
@@ -580,20 +829,33 @@ export class AgentHarnessRuntime {
580
829
  const workspaceId = this.getWorkspaceId(binding);
581
830
  const userId = input.userId ?? "default";
582
831
  const projectId = input.projectId ?? workspaceId;
583
- const threadCandidates = input.candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
584
- const workspaceCandidates = input.candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
585
- const agentCandidates = input.candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
832
+ const existingRecords = await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace", "user", "project"]);
833
+ const transformedCandidates = this.runtimeMemoryManager
834
+ ? await this.runtimeMemoryManager.transform({
835
+ candidates: input.candidates,
836
+ binding,
837
+ threadId: input.threadId,
838
+ runId: input.runId,
839
+ recordedAt: input.recordedAt,
840
+ existingRecords,
841
+ })
842
+ : input.candidates;
843
+ const threadCandidates = transformedCandidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
844
+ const workspaceCandidates = transformedCandidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
845
+ const agentCandidates = transformedCandidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
846
+ const userCandidates = transformedCandidates.filter((candidate) => (candidate.scope ?? "thread") === "user");
847
+ const projectCandidates = transformedCandidates.filter((candidate) => (candidate.scope ?? "thread") === "project");
586
848
  if (input.storeCandidateLog) {
587
849
  await this.runtimeMemoryStore.put(["memories", "candidates", input.threadId], `${input.runId}.json`, {
588
850
  runId: input.runId,
589
851
  threadId: input.threadId,
590
852
  storedAt: input.recordedAt,
591
- candidates: input.candidates,
853
+ candidates: transformedCandidates,
592
854
  });
593
855
  }
594
856
  const persisted = await persistStructuredMemoryRecords({
595
857
  store: this.runtimeMemoryStore,
596
- candidates: input.candidates,
858
+ candidates: transformedCandidates,
597
859
  threadId: input.threadId,
598
860
  runId: input.runId,
599
861
  agentId: input.agentId,
@@ -602,6 +864,7 @@ export class AgentHarnessRuntime {
602
864
  projectId,
603
865
  recordedAt: input.recordedAt,
604
866
  });
867
+ await this.rebuildRuntimeMemoryVectorIndex();
605
868
  const writes = [];
606
869
  if (threadCandidates.length > 0) {
607
870
  writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId: input.threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
@@ -636,6 +899,28 @@ export class AgentHarnessRuntime {
636
899
  config: this.runtimeMemoryMaintenanceConfig ?? undefined,
637
900
  }));
638
901
  }
902
+ if (userCandidates.length > 0) {
903
+ writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("user", binding, { userId }), "tool-memory.md", userCandidates, 20, "User Tool Memory"));
904
+ writes.push(consolidateStructuredMemoryScope({
905
+ store: this.runtimeMemoryStore,
906
+ namespace: this.resolveMemoryNamespace("user", binding, { userId }),
907
+ title: "User Structured Memory",
908
+ scope: "user",
909
+ maxEntries: 20,
910
+ config: this.runtimeMemoryMaintenanceConfig ?? undefined,
911
+ }));
912
+ }
913
+ if (projectCandidates.length > 0) {
914
+ writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("project", binding, { projectId }), "tool-memory.md", projectCandidates, 20, "Project Tool Memory"));
915
+ writes.push(consolidateStructuredMemoryScope({
916
+ store: this.runtimeMemoryStore,
917
+ namespace: this.resolveMemoryNamespace("project", binding, { projectId }),
918
+ title: "Project Structured Memory",
919
+ scope: "project",
920
+ maxEntries: 20,
921
+ config: this.runtimeMemoryMaintenanceConfig ?? undefined,
922
+ }));
923
+ }
639
924
  await Promise.all(writes);
640
925
  return persisted;
641
926
  }
@@ -909,10 +1194,12 @@ export class AgentHarnessRuntime {
909
1194
  this.unregisterThreadMemorySync();
910
1195
  this.unregisterRuntimeMemorySync();
911
1196
  this.unregisterMem0IngestionSync();
1197
+ this.unregisterRuntimeMemoryFormationSync();
912
1198
  await Promise.allSettled(Array.from(this.backgroundTasks));
913
1199
  await this.threadMemorySync?.close();
914
1200
  await this.runtimeMemorySync?.close();
915
1201
  await this.mem0IngestionSync?.close();
1202
+ await this.runtimeMemoryFormationSync?.close();
916
1203
  await closeMcpClientsForWorkspace(this.workspace);
917
1204
  this.initialized = false;
918
1205
  }
@@ -3,7 +3,7 @@ import { MemorySaver } from "@langchain/langgraph";
3
3
  import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";
4
4
  import { FileCheckpointSaver } from "../maintenance/file-checkpoint-saver.js";
5
5
  import { ManagedSqliteSaver } from "../maintenance/sqlite-maintained-checkpoint-saver.js";
6
- import { createInMemoryStore, FileBackedStore } from "../harness/system/store.js";
6
+ import { createInMemoryStore, FileBackedStore, SqliteBackedStore } from "../harness/system/store.js";
7
7
  export function createStoreForConfig(storeConfig, runRoot) {
8
8
  const kind = typeof storeConfig.kind === "string" ? storeConfig.kind : "FileStore";
9
9
  switch (kind) {
@@ -11,6 +11,10 @@ export function createStoreForConfig(storeConfig, runRoot) {
11
11
  const configuredPath = typeof storeConfig.path === "string" ? storeConfig.path : "store.json";
12
12
  return new FileBackedStore(path.isAbsolute(configuredPath) ? configuredPath : path.join(runRoot, configuredPath));
13
13
  }
14
+ case "SqliteStore": {
15
+ const configuredPath = typeof storeConfig.path === "string" ? storeConfig.path : "store.sqlite";
16
+ return new SqliteBackedStore(path.isAbsolute(configuredPath) ? configuredPath : path.join(runRoot, configuredPath));
17
+ }
14
18
  case "InMemoryStore":
15
19
  return createInMemoryStore();
16
20
  case "RedisStore":
@@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises";
3
3
  import { Document } from "@langchain/core/documents";
4
4
  import { createClient } from "@libsql/client";
5
5
  import { LibSQLVectorStore } from "@langchain/community/vectorstores/libsql";
6
+ import { QdrantClient } from "@qdrant/js-client-rest";
6
7
  import { compileVectorStore } from "../../workspace/resource-compilers.js";
7
8
  import { resolveRefId } from "../../workspace/support/workspace-ref-utils.js";
8
9
  import { resolveCompiledEmbeddingModel, resolveCompiledEmbeddingModelRef } from "./embedding-models.js";
@@ -52,6 +53,27 @@ async function ensureLibSqlSchema(db, table, column, dimensions) {
52
53
  ON ${safeTable}(libsql_vector_idx(${safeColumn}))
53
54
  `);
54
55
  }
56
+ function createQdrantClientForStore(vectorStore) {
57
+ return new QdrantClient({
58
+ ...(vectorStore.url ? { url: vectorStore.url } : {}),
59
+ ...(vectorStore.host ? { host: vectorStore.host } : {}),
60
+ ...(typeof vectorStore.port === "number" ? { port: vectorStore.port } : {}),
61
+ ...(vectorStore.authToken ? { apiKey: vectorStore.authToken } : {}),
62
+ checkCompatibility: false,
63
+ });
64
+ }
65
+ async function ensureQdrantCollection(client, collection, dimensions) {
66
+ const collections = await client.getCollections();
67
+ if (collections.collections.some((entry) => entry.name === collection)) {
68
+ return;
69
+ }
70
+ await client.createCollection(collection, {
71
+ vectors: {
72
+ size: dimensions,
73
+ distance: "Cosine",
74
+ },
75
+ });
76
+ }
55
77
  export function resolveCompiledVectorStoreRef(workspace, vectorStoreRef) {
56
78
  const resolvedId = vectorStoreRef ? resolveRefId(vectorStoreRef) : "default";
57
79
  const vectorStore = workspace.vectorStores.get(resolvedId);
@@ -75,6 +97,81 @@ export async function resolveCompiledVectorStore(workspace, vectorStore, options
75
97
  const embeddings = await resolveCompiledEmbeddingModel(embeddingModel, options.embeddingModelResolver);
76
98
  return createLlamaIndexVectorStore(workspace.workspaceRoot, vectorStore, embeddings);
77
99
  }
100
+ if (vectorStore.kind === "QdrantVectorStore") {
101
+ const embeddingModel = resolveCompiledEmbeddingModelRef(workspace, vectorStore.embeddingModelRef);
102
+ const embeddings = await resolveCompiledEmbeddingModel(embeddingModel, options.embeddingModelResolver);
103
+ const client = createQdrantClientForStore(vectorStore);
104
+ const collection = vectorStore.collection ?? "vectors";
105
+ const ensureCollectionForTexts = async (texts) => {
106
+ const seedText = texts.find((text) => text.trim().length > 0) ?? "seed";
107
+ const sample = await embeddings.embedQuery(seedText);
108
+ await ensureQdrantCollection(client, collection, sample.length);
109
+ };
110
+ return {
111
+ kind: vectorStore.kind,
112
+ embeddingModelRef: vectorStore.embeddingModelRef,
113
+ addDocuments: async (documents) => {
114
+ if (documents.length === 0) {
115
+ return [];
116
+ }
117
+ await ensureCollectionForTexts(documents.map((document) => document.pageContent));
118
+ const vectors = await embeddings.embedDocuments(documents.map((document) => document.pageContent));
119
+ const ids = documents.map((_, index) => `${Date.now()}-${index}-${Math.random().toString(36).slice(2, 10)}`);
120
+ await client.upsert(collection, {
121
+ wait: true,
122
+ points: documents.map((document, index) => ({
123
+ id: ids[index] ?? index,
124
+ vector: vectors[index] ?? [],
125
+ payload: {
126
+ pageContent: document.pageContent,
127
+ ...(document.metadata ? { metadata: document.metadata } : {}),
128
+ },
129
+ })),
130
+ });
131
+ return ids;
132
+ },
133
+ similaritySearch: async (query, limit) => {
134
+ const collections = await client.getCollections();
135
+ if (!collections.collections.some((entry) => entry.name === collection)) {
136
+ return [];
137
+ }
138
+ const queryVector = await embeddings.embedQuery(query);
139
+ const rows = await client.search(collection, {
140
+ vector: queryVector,
141
+ limit,
142
+ with_payload: true,
143
+ });
144
+ return rows.map((row) => {
145
+ const payload = (row.payload ?? {});
146
+ return {
147
+ pageContent: typeof payload.pageContent === "string" ? payload.pageContent : "",
148
+ metadata: typeof payload.metadata === "object" && payload.metadata && !Array.isArray(payload.metadata)
149
+ ? payload.metadata
150
+ : {},
151
+ score: typeof row.score === "number" ? row.score : undefined,
152
+ };
153
+ });
154
+ },
155
+ delete: async (params) => {
156
+ const collections = await client.getCollections();
157
+ if (!collections.collections.some((entry) => entry.name === collection)) {
158
+ return;
159
+ }
160
+ if (params.deleteAll) {
161
+ await client.deleteCollection(collection);
162
+ return;
163
+ }
164
+ const pointIds = (params.ids ?? []).filter((id) => typeof id === "string" || typeof id === "number");
165
+ if (pointIds.length === 0) {
166
+ return;
167
+ }
168
+ await client.delete(collection, {
169
+ wait: true,
170
+ points: pointIds,
171
+ });
172
+ },
173
+ };
174
+ }
78
175
  if (vectorStore.kind !== "LibSQLVectorStore") {
79
176
  throw new Error(`Vector store kind ${vectorStore.kind} is not supported by the built-in runtime.`);
80
177
  }