@botbotgo/agent-harness 0.0.153 → 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);
@@ -243,6 +292,75 @@ export class AgentHarnessRuntime {
243
292
  async listRuns(filter) {
244
293
  return this.persistence.listRuns(filter);
245
294
  }
295
+ async memorize(input) {
296
+ const binding = this.defaultRuntimeEntryBinding;
297
+ if (!binding) {
298
+ throw new Error("memorize requires a runtime entry binding.");
299
+ }
300
+ if (!Array.isArray(input.records)) {
301
+ throw new Error("memorize requires input.records to be an array.");
302
+ }
303
+ const candidates = input.records
304
+ .filter((record) => typeof record === "object" && record !== null)
305
+ .filter((record) => record.noStore !== true);
306
+ if (candidates.length === 0) {
307
+ return { records: [], decisions: [] };
308
+ }
309
+ if (candidates.some((record) => typeof record.content !== "string" || record.content.trim().length === 0)) {
310
+ throw new Error("memorize requires every record to include non-empty content.");
311
+ }
312
+ if (candidates.some((record) => (record.scope ?? "thread") === "thread") && !input.threadId) {
313
+ throw new Error("memorize requires threadId when storing thread-scoped memory.");
314
+ }
315
+ const recordedAt = input.recordedAt ?? new Date().toISOString();
316
+ const runId = input.runId ?? createPersistentId(new Date(recordedAt));
317
+ const threadId = input.threadId ?? `memory-api-${runId}`;
318
+ return this.persistStructuredMemoryCandidates(binding, {
319
+ candidates: candidates.map((record) => ({ ...record, content: record.content.trim() })),
320
+ threadId,
321
+ runId,
322
+ agentId: input.agentId ?? binding.agent.id,
323
+ userId: input.userId,
324
+ projectId: input.projectId,
325
+ recordedAt,
326
+ storeCandidateLog: true,
327
+ });
328
+ }
329
+ async recall(input) {
330
+ const binding = this.defaultRuntimeEntryBinding;
331
+ if (!binding) {
332
+ throw new Error("recall requires a runtime entry binding.");
333
+ }
334
+ if (typeof input.query !== "string" || input.query.trim().length === 0) {
335
+ throw new Error("recall requires a non-empty query.");
336
+ }
337
+ const workspaceId = this.getWorkspaceId(binding);
338
+ const agentId = input.agentId ?? binding.agent.id;
339
+ const userId = input.userId ?? "default";
340
+ const projectId = input.projectId ?? workspaceId;
341
+ const scopes = this.resolveRecallScopes(input);
342
+ const topK = typeof input.topK === "number" && Number.isInteger(input.topK) && input.topK > 0
343
+ ? input.topK
344
+ : (this.runtimeMemoryPolicy?.retrieval.defaultTopK ?? 5);
345
+ const kinds = input.kinds?.length ? new Set(input.kinds) : null;
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
+ },
359
+ }))
360
+ .slice(0, topK)
361
+ .map(({ record }) => record);
362
+ return { items };
363
+ }
246
364
  async getRun(runId) {
247
365
  return this.persistence.getRun(runId);
248
366
  }
@@ -392,8 +510,7 @@ export class AgentHarnessRuntime {
392
510
  }
393
511
  }
394
512
  resolveMemoryNamespace(scope, binding, options = {}) {
395
- const workspaceRoot = binding.harnessRuntime.workspaceRoot ?? this.workspace.workspaceRoot;
396
- const workspaceId = path.basename(workspaceRoot) || "default";
513
+ const workspaceId = this.getWorkspaceId(binding);
397
514
  const template = this.runtimeMemoryPolicy?.namespaces[scope] ?? `memories/${scope}s/{${scope}Id}`;
398
515
  return resolveMemoryNamespace(template, {
399
516
  threadId: options.threadId,
@@ -403,36 +520,280 @@ export class AgentHarnessRuntime {
403
520
  projectId: options.projectId ?? workspaceId,
404
521
  });
405
522
  }
406
- async buildRuntimeMemoryContext(binding, threadId, input) {
407
- const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
408
- const ranked = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace"]))
409
- .filter((record) => {
410
- if (record.status !== "active") {
411
- return false;
523
+ getWorkspaceId(binding) {
524
+ const workspaceRoot = binding.harnessRuntime.workspaceRoot ?? this.workspace.workspaceRoot;
525
+ return path.basename(workspaceRoot) || "default";
526
+ }
527
+ resolveRecallScopes(input) {
528
+ if (Array.isArray(input.scopes) && input.scopes.length > 0) {
529
+ return Array.from(new Set(input.scopes));
530
+ }
531
+ const scopes = new Set(["thread", "agent", "workspace"]);
532
+ if (input.userId) {
533
+ scopes.add("user");
534
+ }
535
+ if (input.projectId) {
536
+ scopes.add("project");
537
+ }
538
+ return Array.from(scopes);
539
+ }
540
+ matchesRecallScope(record, filters) {
541
+ if (record.scope === "thread") {
542
+ return typeof filters.threadId === "string" && String(record.provenance.threadId ?? "") === filters.threadId;
543
+ }
544
+ if (record.scope === "agent") {
545
+ return String(record.provenance.agentId ?? "") === filters.agentId;
546
+ }
547
+ if (record.scope === "workspace") {
548
+ return String(record.provenance.workspaceId ?? filters.workspaceId) === filters.workspaceId;
549
+ }
550
+ if (record.scope === "user") {
551
+ return String(record.provenance.userId ?? "default") === filters.userId;
552
+ }
553
+ return String(record.provenance.projectId ?? filters.workspaceId) === filters.projectId;
554
+ }
555
+ getMemoryScopeBoost(scope) {
556
+ if (scope === "thread") {
557
+ return 4;
558
+ }
559
+ if (scope === "agent") {
560
+ return 2;
561
+ }
562
+ if (scope === "workspace") {
563
+ return 1;
564
+ }
565
+ return 0;
566
+ }
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;
412
588
  }
413
- if (record.scope === "thread") {
414
- 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
+ }
415
686
  }
416
- if (record.scope === "agent") {
417
- return String(record.provenance.agentId ?? "") === binding.agent.id;
687
+ catch {
688
+ // Fail open to lexical/runtime ranking.
418
689
  }
419
- return true;
420
- })
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
+ }))
421
775
  .map((record) => {
422
- const scopeBoost = record.scope === "thread" ? 4 : record.scope === "agent" ? 2 : 1;
423
- 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;
424
783
  return {
425
784
  content: [
426
- `# Durable ${record.scope[0].toUpperCase()}${record.scope.slice(1)} Memory`,
785
+ `# Durable ${record.record.scope[0].toUpperCase()}${record.record.scope.slice(1)} Memory`,
427
786
  "",
428
- `- summary: ${record.summary}`,
429
- `- kind: ${record.kind}`,
430
- `- scope: ${record.scope}`,
431
- `- 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)}`,
432
791
  "",
433
- record.content,
792
+ record.record.content,
434
793
  ].join("\n"),
435
- 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,
436
797
  };
437
798
  })
438
799
  .sort((left, right) => right.score - left.score)
@@ -446,36 +807,70 @@ export class AgentHarnessRuntime {
446
807
  if (!Array.isArray(value) || value.length === 0) {
447
808
  return;
448
809
  }
810
+ if (this.runtimeMemoryFormationConfig?.hotPath.enabled === false) {
811
+ return;
812
+ }
449
813
  const candidates = value
450
814
  .filter((candidate) => typeof candidate === "object" && candidate !== null)
451
815
  .filter((candidate) => candidate.noStore !== true && typeof candidate.content === "string" && candidate.content.trim().length > 0);
452
816
  if (candidates.length === 0) {
453
817
  return;
454
818
  }
455
- const threadCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
456
- const workspaceCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
457
- const agentCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
458
- const recordedAt = new Date().toISOString();
459
- await this.runtimeMemoryStore.put(["memories", "candidates", threadId], `${runId}.json`, {
460
- runId,
461
- threadId,
462
- storedAt: recordedAt,
463
- candidates,
464
- });
465
- await persistStructuredMemoryRecords({
466
- store: this.runtimeMemoryStore,
819
+ await this.persistStructuredMemoryCandidates(binding, {
467
820
  candidates,
468
821
  threadId,
469
822
  runId,
470
823
  agentId: binding.agent.id,
471
- recordedAt,
824
+ recordedAt: new Date().toISOString(),
825
+ storeCandidateLog: true,
826
+ });
827
+ }
828
+ async persistStructuredMemoryCandidates(binding, input) {
829
+ const workspaceId = this.getWorkspaceId(binding);
830
+ const userId = input.userId ?? "default";
831
+ const projectId = input.projectId ?? workspaceId;
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");
848
+ if (input.storeCandidateLog) {
849
+ await this.runtimeMemoryStore.put(["memories", "candidates", input.threadId], `${input.runId}.json`, {
850
+ runId: input.runId,
851
+ threadId: input.threadId,
852
+ storedAt: input.recordedAt,
853
+ candidates: transformedCandidates,
854
+ });
855
+ }
856
+ const persisted = await persistStructuredMemoryRecords({
857
+ store: this.runtimeMemoryStore,
858
+ candidates: transformedCandidates,
859
+ threadId: input.threadId,
860
+ runId: input.runId,
861
+ agentId: input.agentId,
862
+ workspaceId,
863
+ userId,
864
+ projectId,
865
+ recordedAt: input.recordedAt,
472
866
  });
867
+ await this.rebuildRuntimeMemoryVectorIndex();
473
868
  const writes = [];
474
869
  if (threadCandidates.length > 0) {
475
- writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
870
+ writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId: input.threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
476
871
  writes.push(consolidateStructuredMemoryScope({
477
872
  store: this.runtimeMemoryStore,
478
- namespace: this.resolveMemoryNamespace("thread", binding, { threadId }),
873
+ namespace: this.resolveMemoryNamespace("thread", binding, { threadId: input.threadId }),
479
874
  title: "Thread Structured Memory",
480
875
  scope: "thread",
481
876
  maxEntries: 12,
@@ -504,7 +899,30 @@ export class AgentHarnessRuntime {
504
899
  config: this.runtimeMemoryMaintenanceConfig ?? undefined,
505
900
  }));
506
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
+ }
507
924
  await Promise.all(writes);
925
+ return persisted;
508
926
  }
509
927
  async appendMemoryDigest(namespace, key, candidates, maxEntries, title) {
510
928
  const existing = await this.runtimeMemoryStore.get(namespace, key);
@@ -776,10 +1194,12 @@ export class AgentHarnessRuntime {
776
1194
  this.unregisterThreadMemorySync();
777
1195
  this.unregisterRuntimeMemorySync();
778
1196
  this.unregisterMem0IngestionSync();
1197
+ this.unregisterRuntimeMemoryFormationSync();
779
1198
  await Promise.allSettled(Array.from(this.backgroundTasks));
780
1199
  await this.threadMemorySync?.close();
781
1200
  await this.runtimeMemorySync?.close();
782
1201
  await this.mem0IngestionSync?.close();
1202
+ await this.runtimeMemoryFormationSync?.close();
783
1203
  await closeMcpClientsForWorkspace(this.workspace);
784
1204
  this.initialized = false;
785
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":