@botbotgo/agent-harness 0.0.128 → 0.0.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,7 +109,11 @@ Boundary documents live in:
109
109
 
110
110
  - `docs/upstream-feature-matrix.md`
111
111
  - `docs/product-boundary.md`
112
+ - `docs/runtime-blueprint-assessment.md`
113
+ - `docs/policy-engine-scope.md`
112
114
  - `docs/acp-support-plan.md`
115
+ - `docs/recovery-policy-matrix.md`
116
+ - `docs/operator-timeline.md`
113
117
  - `docs/feature-checklist.md`
114
118
  - `docs/long-term-memory.md`
115
119
  - `docs/app-task-pattern.md`
package/README.zh.md CHANGED
@@ -109,7 +109,11 @@ AI 让 agent 逻辑、工具调用和工作流代码更容易生成,真正变
109
109
 
110
110
  - `docs/upstream-feature-matrix.md`
111
111
  - `docs/product-boundary.md`
112
+ - `docs/runtime-blueprint-assessment.md`
113
+ - `docs/policy-engine-scope.md`
112
114
  - `docs/acp-support-plan.md`
115
+ - `docs/recovery-policy-matrix.md`
116
+ - `docs/operator-timeline.md`
113
117
  - `docs/feature-checklist.md`
114
118
  - `docs/long-term-memory.md`
115
119
  - `docs/app-task-pattern.md`
@@ -63,3 +63,11 @@ spec:
63
63
  backgroundConsolidation: true
64
64
  writeOnApprovalResolution: true
65
65
  writeOnRunCompletion: true
66
+
67
+ # agent-harness feature: optional Mem0 OSS ingestion engine for automatic long-term knowledge extraction.
68
+ mem0:
69
+ enabled: false
70
+ apiKeyEnv: MEM0_API_KEY
71
+ appId: agent-harness
72
+ stateStorePath: mem0-sync-state.json
73
+ maxMessagesPerRun: 200
@@ -40,6 +40,25 @@ export type HarnessEventProjection = {
40
40
  shouldHandle?: (event: HarnessEvent) => boolean;
41
41
  handleEvent: HarnessEventListener;
42
42
  };
43
+ /**
44
+ * Operator-facing timeline item projected from persisted runtime events.
45
+ * This is an inspection convenience over runtime records, not a new execution protocol.
46
+ */
47
+ export type RuntimeTimelineItem = {
48
+ eventId: string;
49
+ threadId: string;
50
+ runId: string;
51
+ eventType: HarnessEventType;
52
+ timestamp: string;
53
+ sequence: number;
54
+ source: HarnessEvent["source"];
55
+ kind: "run" | "queue" | "approval" | "recovery" | "artifact" | "other";
56
+ payload: Record<string, unknown>;
57
+ };
58
+ export type RuntimeTimelineProjectionOptions = {
59
+ threadId?: string;
60
+ runId?: string;
61
+ };
43
62
  export type RuntimeEventSink = {
44
63
  publish: (event: HarnessEvent) => void;
45
64
  subscribe: (listener: HarnessEventListener) => () => void;
@@ -131,7 +131,7 @@ spec:
131
131
  kind: StoreBackend
132
132
  `;
133
133
  }
134
- function renderRuntimeMemoryYaml() {
134
+ function renderRuntimeMemoryYaml(projectSlug) {
135
135
  return `apiVersion: agent-harness/v1alpha1
136
136
  kind: RuntimeMemory
137
137
  metadata:
@@ -156,6 +156,16 @@ spec:
156
156
  - scratchpad
157
157
  - transient_reasoning
158
158
  - intermediate_results
159
+ ingestion:
160
+ backgroundConsolidation: true
161
+ writeOnApprovalResolution: true
162
+ writeOnRunCompletion: true
163
+ mem0:
164
+ enabled: false
165
+ apiKeyEnv: MEM0_API_KEY
166
+ appId: ${projectSlug}
167
+ stateStorePath: mem0-sync-state.json
168
+ maxMessagesPerRun: 200
159
169
  `;
160
170
  }
161
171
  function renderToolsYaml(options) {
@@ -321,7 +331,7 @@ export async function initProject(projectRoot, projectName, options = {}) {
321
331
  ["config/runtime/workspace.yaml", renderWorkspaceYaml()],
322
332
  ["config/agent-context.md", renderAgentContext(resolved)],
323
333
  ["config/catalogs/models.yaml", renderModelsYaml(resolved)],
324
- ["config/runtime/runtime-memory.yaml", renderRuntimeMemoryYaml()],
334
+ ["config/runtime/runtime-memory.yaml", renderRuntimeMemoryYaml(projectSlug)],
325
335
  ["config/catalogs/backends.yaml", renderBackendsYaml()],
326
336
  ["config/catalogs/tools.yaml", renderToolsYaml(resolved)],
327
337
  ["config/agents/research.yaml", renderResearchAgentYaml(resolved)],
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.127";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.129";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.127";
1
+ export const AGENT_HARNESS_VERSION = "0.0.129";
@@ -0,0 +1,2 @@
1
+ import type { HarnessEvent, RuntimeTimelineItem, RuntimeTimelineProjectionOptions } from "../../../contracts/types.js";
2
+ export declare function projectRuntimeTimeline(events: readonly HarnessEvent[], options?: RuntimeTimelineProjectionOptions): RuntimeTimelineItem[];
@@ -0,0 +1,52 @@
1
+ function classifyTimelineItem(event) {
2
+ switch (event.eventType) {
3
+ case "run.queued":
4
+ case "run.dequeued":
5
+ return "queue";
6
+ case "approval.requested":
7
+ case "approval.resolved":
8
+ return "approval";
9
+ case "run.resumed":
10
+ return "recovery";
11
+ case "artifact.created":
12
+ return "artifact";
13
+ case "run.created":
14
+ case "run.state.changed":
15
+ return "run";
16
+ default:
17
+ return "other";
18
+ }
19
+ }
20
+ export function projectRuntimeTimeline(events, options = {}) {
21
+ const filtered = events.filter((event) => {
22
+ if (options.threadId && event.threadId !== options.threadId) {
23
+ return false;
24
+ }
25
+ if (options.runId && event.runId !== options.runId) {
26
+ return false;
27
+ }
28
+ return true;
29
+ });
30
+ return [...filtered]
31
+ .sort((left, right) => {
32
+ const timestampComparison = left.timestamp.localeCompare(right.timestamp);
33
+ if (timestampComparison !== 0) {
34
+ return timestampComparison;
35
+ }
36
+ if (left.sequence !== right.sequence) {
37
+ return left.sequence - right.sequence;
38
+ }
39
+ return left.eventId.localeCompare(right.eventId);
40
+ })
41
+ .map((event) => ({
42
+ eventId: event.eventId,
43
+ threadId: event.threadId,
44
+ runId: event.runId,
45
+ eventType: event.eventType,
46
+ timestamp: event.timestamp,
47
+ sequence: event.sequence,
48
+ source: event.source,
49
+ kind: classifyTimelineItem(event),
50
+ payload: event.payload,
51
+ }));
52
+ }
@@ -1,6 +1,7 @@
1
1
  export * from "./events/event-bus.js";
2
2
  export * from "./events/event-sink.js";
3
3
  export * from "./events/events.js";
4
+ export * from "./events/timeline.js";
4
5
  export * from "./system/health-monitor.js";
5
6
  export * from "./run/helpers.js";
6
7
  export * from "./system/inventory.js";
@@ -1,6 +1,7 @@
1
1
  export * from "./events/event-bus.js";
2
2
  export * from "./events/event-sink.js";
3
3
  export * from "./events/events.js";
4
+ export * from "./events/timeline.js";
4
5
  export * from "./system/health-monitor.js";
5
6
  export * from "./run/helpers.js";
6
7
  export * from "./system/inventory.js";
@@ -41,6 +41,17 @@ async function executeRecoveredRun(context, input) {
41
41
  }
42
42
  return true;
43
43
  }
44
+ async function failResumingRecovery(context, thread, checkpointRef, error) {
45
+ await context.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", checkpointRef);
46
+ await context.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
47
+ await context.emit(thread.threadId, thread.latestRunId, 101, "run.state.changed", {
48
+ previousState: "resuming",
49
+ state: "failed",
50
+ checkpointRef,
51
+ ...(error ? { error } : {}),
52
+ });
53
+ return true;
54
+ }
44
55
  export async function recoverQueuedStartupRun(context, thread) {
45
56
  if (thread.status !== "queued") {
46
57
  return false;
@@ -113,9 +124,7 @@ export async function recoverResumingStartupRun(context, thread) {
113
124
  return true;
114
125
  }
115
126
  if (recoveryIntent.attempts >= context.recoveryConfig.maxRecoveryAttempts) {
116
- await context.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
117
- await context.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
118
- return true;
127
+ return failResumingRecovery(context, thread, recoveryIntent.checkpointRef, "recovery attempts exhausted");
119
128
  }
120
129
  await context.persistence.saveRecoveryIntent(thread.threadId, thread.latestRunId, {
121
130
  ...recoveryIntent,
@@ -143,14 +152,7 @@ export async function recoverResumingStartupRun(context, thread) {
143
152
  catch (error) {
144
153
  context.recordLlmFailure(startedAt);
145
154
  if (recoveryIntent.attempts + 1 >= context.recoveryConfig.maxRecoveryAttempts) {
146
- await context.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
147
- await context.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
148
- await context.emit(thread.threadId, thread.latestRunId, 101, "run.state.changed", {
149
- previousState: "resuming",
150
- state: "failed",
151
- checkpointRef: recoveryIntent.checkpointRef,
152
- error: error instanceof Error ? error.message : String(error),
153
- });
155
+ await failResumingRecovery(context, thread, recoveryIntent.checkpointRef, error instanceof Error ? error.message : String(error));
154
156
  }
155
157
  }
156
158
  return true;
@@ -1,5 +1,6 @@
1
1
  import { AGENT_INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError } from "../../agent-runtime-adapter.js";
2
2
  import { renderRuntimeFailure, renderToolFailure } from "../../support/harness-support.js";
3
+ import { getBindingPrimaryModel } from "../../support/compiled-binding.js";
3
4
  import { createContentBlocksItem, createToolResultKey, } from "../events/streaming.js";
4
5
  function normalizeStreamChunk(chunk) {
5
6
  if (typeof chunk === "string") {
@@ -27,12 +28,21 @@ function normalizeStreamChunk(chunk) {
27
28
  }
28
29
  return { kind: "content", content: chunk.content ?? "" };
29
30
  }
31
+ function isOpenAICompatibleStreamingCompatibilityError(binding, error) {
32
+ const primaryModel = getBindingPrimaryModel(binding);
33
+ if (primaryModel?.provider !== "openai-compatible") {
34
+ return false;
35
+ }
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return message.toLowerCase().includes("received empty response from chat model call");
38
+ }
30
39
  export async function* streamHarnessRun(options) {
31
40
  const priorHistoryPromise = Promise.resolve(options.isNewThread ? [] : undefined).then((historyHint) => historyHint ?? options.loadPriorHistory(options.threadId, options.runId));
32
41
  yield { type: "event", event: await options.runCreatedEventPromise };
33
42
  let releaseRunSlot = async () => undefined;
34
43
  let emitted = false;
35
44
  let streamActivityObserved = false;
45
+ let nonUpstreamStreamActivityObserved = false;
36
46
  try {
37
47
  const [priorHistory, acquiredReleaseRunSlot] = await Promise.all([
38
48
  priorHistoryPromise,
@@ -63,6 +73,7 @@ export async function* streamHarnessRun(options) {
63
73
  };
64
74
  continue;
65
75
  }
76
+ nonUpstreamStreamActivityObserved = true;
66
77
  if (normalizedChunk.kind === "interrupt") {
67
78
  const checkpointRef = `checkpoints/${options.threadId}/${options.runId}/cp-1`;
68
79
  const waitingEvent = await options.setRunStateAndEmit(options.threadId, options.runId, 6, "waiting_for_approval", {
@@ -148,7 +159,10 @@ export async function* streamHarnessRun(options) {
148
159
  };
149
160
  }
150
161
  catch (error) {
151
- if (emitted || streamActivityObserved) {
162
+ const shouldRetryAfterStreamingCompatibilityError = streamActivityObserved &&
163
+ !nonUpstreamStreamActivityObserved &&
164
+ isOpenAICompatibleStreamingCompatibilityError(options.binding, error);
165
+ if ((emitted || streamActivityObserved) && !shouldRetryAfterStreamingCompatibilityError) {
152
166
  const runtimeFailure = renderRuntimeFailure(error);
153
167
  yield {
154
168
  type: "event",
@@ -0,0 +1,42 @@
1
+ import { type Message as Mem0Message } from "mem0ai";
2
+ import type { HarnessEvent, HarnessEventProjection } from "../../../contracts/types.js";
3
+ import type { RuntimePersistence } from "../../../persistence/types.js";
4
+ import { type StoreLike } from "./store.js";
5
+ export type ResolvedMem0Config = {
6
+ enabled: true;
7
+ apiKeyEnv: string;
8
+ host?: string;
9
+ organizationName?: string;
10
+ projectName?: string;
11
+ organizationId?: string;
12
+ projectId?: string;
13
+ appId: string;
14
+ userIdPrefix?: string;
15
+ stateStorePath: string;
16
+ maxMessagesPerRun: number;
17
+ writeOnApprovalResolution: boolean;
18
+ writeOnRunCompletion: boolean;
19
+ };
20
+ type Mem0ClientLike = {
21
+ add(messages: Mem0Message[], options?: Record<string, unknown>): Promise<unknown>;
22
+ };
23
+ type Mem0ClientFactory = (config: ResolvedMem0Config) => Promise<Mem0ClientLike> | Mem0ClientLike;
24
+ export declare function readMem0RuntimeConfig(runtimeMemory: Record<string, unknown> | undefined, workspaceRoot: string): ResolvedMem0Config | undefined;
25
+ export declare class Mem0IngestionSync implements HarnessEventProjection {
26
+ private readonly persistence;
27
+ private readonly config;
28
+ private readonly runRoot;
29
+ private readonly stateStore;
30
+ private readonly clientFactory;
31
+ private readonly pending;
32
+ private syncChain;
33
+ private clientPromise;
34
+ readonly name = "mem0-ingestion-sync";
35
+ constructor(persistence: RuntimePersistence, config: ResolvedMem0Config, runRoot: string, stateStore?: StoreLike, clientFactory?: Mem0ClientFactory);
36
+ shouldHandle(event: HarnessEvent): boolean;
37
+ handleEvent(event: HarnessEvent): Promise<void>;
38
+ private getClient;
39
+ private syncRun;
40
+ close(): Promise<void>;
41
+ }
42
+ export {};
@@ -0,0 +1,181 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { MemoryClient } from "mem0ai";
4
+ import { extractMessageText } from "../../../utils/message-content.js";
5
+ import { FileBackedStore } from "./store.js";
6
+ const MEM0_EVENT_TYPES = new Set(["run.state.changed", "approval.resolved"]);
7
+ function asRecord(value) {
8
+ return typeof value === "object" && value && !Array.isArray(value) ? value : undefined;
9
+ }
10
+ function asNonEmptyString(value) {
11
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
+ }
13
+ function asPositiveInteger(value) {
14
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
15
+ }
16
+ function asBoolean(value) {
17
+ return typeof value === "boolean" ? value : undefined;
18
+ }
19
+ function normalizeMessages(messages) {
20
+ const normalized = [];
21
+ for (const message of messages) {
22
+ const content = extractMessageText(message.content).trim();
23
+ if (!content) {
24
+ continue;
25
+ }
26
+ normalized.push({
27
+ role: message.role,
28
+ content,
29
+ });
30
+ }
31
+ return normalized;
32
+ }
33
+ function fingerprintMessages(messages) {
34
+ const hash = createHash("sha256");
35
+ for (const message of messages) {
36
+ hash.update(message.role);
37
+ hash.update("\n");
38
+ hash.update(message.createdAt);
39
+ hash.update("\n");
40
+ hash.update(extractMessageText(message.content));
41
+ hash.update("\n---\n");
42
+ }
43
+ return hash.digest("hex");
44
+ }
45
+ export function readMem0RuntimeConfig(runtimeMemory, workspaceRoot) {
46
+ if (runtimeMemory?.enabled !== true) {
47
+ return undefined;
48
+ }
49
+ const mem0 = asRecord(runtimeMemory.mem0);
50
+ if (!mem0 || mem0.enabled !== true) {
51
+ return undefined;
52
+ }
53
+ const ingestion = asRecord(runtimeMemory.ingestion);
54
+ const workspaceBaseName = path.basename(workspaceRoot) || "agent-harness";
55
+ return {
56
+ enabled: true,
57
+ apiKeyEnv: asNonEmptyString(mem0.apiKeyEnv) ?? "MEM0_API_KEY",
58
+ host: asNonEmptyString(mem0.host),
59
+ organizationName: asNonEmptyString(mem0.organizationName),
60
+ projectName: asNonEmptyString(mem0.projectName),
61
+ organizationId: asNonEmptyString(mem0.organizationId),
62
+ projectId: asNonEmptyString(mem0.projectId),
63
+ appId: asNonEmptyString(mem0.appId) ?? workspaceBaseName,
64
+ userIdPrefix: asNonEmptyString(mem0.userIdPrefix),
65
+ stateStorePath: asNonEmptyString(mem0.stateStorePath) ?? "mem0-sync-state.json",
66
+ maxMessagesPerRun: asPositiveInteger(mem0.maxMessagesPerRun) ?? 200,
67
+ writeOnApprovalResolution: asBoolean(mem0.writeOnApprovalResolution) ?? asBoolean(ingestion?.writeOnApprovalResolution) ?? true,
68
+ writeOnRunCompletion: asBoolean(mem0.writeOnRunCompletion) ?? asBoolean(ingestion?.writeOnRunCompletion) ?? true,
69
+ };
70
+ }
71
+ async function createDefaultMem0Client(config) {
72
+ const apiKey = process.env[config.apiKeyEnv];
73
+ if (!apiKey) {
74
+ throw new Error(`runtimeMemory.mem0 is enabled but environment variable ${config.apiKeyEnv} is not set`);
75
+ }
76
+ return new MemoryClient({
77
+ apiKey,
78
+ ...(config.host ? { host: config.host } : {}),
79
+ ...(config.organizationName ? { organizationName: config.organizationName } : {}),
80
+ ...(config.projectName ? { projectName: config.projectName } : {}),
81
+ ...(config.organizationId ? { organizationId: config.organizationId } : {}),
82
+ ...(config.projectId ? { projectId: config.projectId } : {}),
83
+ });
84
+ }
85
+ export class Mem0IngestionSync {
86
+ persistence;
87
+ config;
88
+ runRoot;
89
+ stateStore;
90
+ clientFactory;
91
+ pending = new Set();
92
+ syncChain = Promise.resolve();
93
+ clientPromise = null;
94
+ name = "mem0-ingestion-sync";
95
+ constructor(persistence, config, runRoot, stateStore = new FileBackedStore(path.join(runRoot, config.stateStorePath)), clientFactory = createDefaultMem0Client) {
96
+ this.persistence = persistence;
97
+ this.config = config;
98
+ this.runRoot = runRoot;
99
+ this.stateStore = stateStore;
100
+ this.clientFactory = clientFactory;
101
+ }
102
+ shouldHandle(event) {
103
+ if (!MEM0_EVENT_TYPES.has(event.eventType)) {
104
+ return false;
105
+ }
106
+ if (event.eventType === "approval.resolved") {
107
+ return this.config.writeOnApprovalResolution;
108
+ }
109
+ if (event.eventType === "run.state.changed") {
110
+ return this.config.writeOnRunCompletion && event.payload.state === "completed";
111
+ }
112
+ return false;
113
+ }
114
+ async handleEvent(event) {
115
+ if (!this.shouldHandle(event)) {
116
+ return;
117
+ }
118
+ const trigger = event.eventType === "approval.resolved" ? "approval.resolved" : "run.completed";
119
+ const task = this.syncChain
120
+ .then(() => this.syncRun(event.threadId, event.runId, trigger))
121
+ .catch(() => {
122
+ // Fail open: memory ingestion must not break run execution.
123
+ });
124
+ this.syncChain = task
125
+ .catch(() => {
126
+ // Fail open: memory ingestion must not break run execution.
127
+ })
128
+ .finally(() => {
129
+ this.pending.delete(task);
130
+ });
131
+ this.pending.add(task);
132
+ }
133
+ async getClient() {
134
+ if (!this.clientPromise) {
135
+ this.clientPromise = Promise.resolve(this.clientFactory(this.config));
136
+ }
137
+ return this.clientPromise;
138
+ }
139
+ async syncRun(threadId, runId, trigger) {
140
+ const allMessages = await this.persistence.listThreadMessages(threadId, this.config.maxMessagesPerRun);
141
+ const runMessages = allMessages.filter((message) => message.runId === runId);
142
+ const normalized = normalizeMessages(runMessages);
143
+ if (normalized.length === 0) {
144
+ return;
145
+ }
146
+ const fingerprint = fingerprintMessages(runMessages);
147
+ const namespace = ["mem0", "threads", threadId, "runs"];
148
+ const existingCursor = await this.stateStore.get(namespace, runId);
149
+ const existing = existingCursor?.value;
150
+ if (existing?.fingerprint === fingerprint) {
151
+ return;
152
+ }
153
+ const [thread, run, client] = await Promise.all([
154
+ this.persistence.getSession(threadId),
155
+ this.persistence.getRun(runId),
156
+ this.getClient(),
157
+ ]);
158
+ await client.add(normalized, {
159
+ ...(this.config.userIdPrefix ? { user_id: `${this.config.userIdPrefix}${threadId}` } : {}),
160
+ run_id: runId,
161
+ agent_id: run?.agentId ?? thread?.agentId,
162
+ app_id: this.config.appId,
163
+ metadata: {
164
+ source: "agent-harness",
165
+ threadId,
166
+ runId,
167
+ agentId: run?.agentId ?? thread?.agentId,
168
+ trigger,
169
+ },
170
+ });
171
+ await this.stateStore.put(namespace, runId, {
172
+ fingerprint,
173
+ messageCount: normalized.length,
174
+ syncedAt: new Date().toISOString(),
175
+ trigger,
176
+ });
177
+ }
178
+ async close() {
179
+ await Promise.allSettled(Array.from(this.pending));
180
+ }
181
+ }
@@ -4,6 +4,11 @@ export type PolicyEngineDecision = {
4
4
  reasons: string[];
5
5
  };
6
6
  export declare class PolicyEngine {
7
+ /**
8
+ * Runtime governance gate for a compiled binding.
9
+ * This engine may allow or block execution with reasons, but it must not
10
+ * redefine upstream planning or execution semantics.
11
+ */
7
12
  evaluate(binding: CompiledAgentBinding): PolicyEngineDecision;
8
13
  }
9
14
  export { PolicyEngineDecision as GovernanceDecision, PolicyEngine as GovernanceEngine, };
@@ -1,5 +1,10 @@
1
1
  import { getPolicyEvaluators } from "../../../extensions.js";
2
2
  export class PolicyEngine {
3
+ /**
4
+ * Runtime governance gate for a compiled binding.
5
+ * This engine may allow or block execution with reasons, but it must not
6
+ * redefine upstream planning or execution semantics.
7
+ */
3
8
  evaluate(binding) {
4
9
  const reasons = [];
5
10
  let allowed = true;
@@ -23,6 +23,8 @@ export declare class AgentHarnessRuntime {
23
23
  private readonly routingDefaultAgentId?;
24
24
  private readonly threadMemorySync;
25
25
  private readonly unregisterThreadMemorySync;
26
+ private readonly mem0IngestionSync;
27
+ private readonly unregisterMem0IngestionSync;
26
28
  private readonly resolvedRuntimeAdapterOptions;
27
29
  private readonly healthMonitor;
28
30
  private readonly recoveryConfig;
@@ -25,6 +25,7 @@ import { getBindingRuntimeExecutionMode, } from "./support/compiled-binding.js";
25
25
  import { bindingSupportsRunningReplay, getWorkspaceBinding, resolveWorkspaceAgentTools, } from "./harness/bindings.js";
26
26
  import { describeWorkspaceInventory, getAgentInventoryRecord, listAgentSkills as listWorkspaceAgentSkills, } from "./harness/system/inventory.js";
27
27
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
28
+ import { Mem0IngestionSync, readMem0RuntimeConfig } from "./harness/system/mem0-ingestion-sync.js";
28
29
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
29
30
  import { initializeHarnessRuntime, reclaimExpiredClaimedRuns as reclaimHarnessExpiredClaimedRuns, recoverStartupRuns as recoverHarnessStartupRuns, isStaleRunningRun as isHarnessStaleRunningRun, } from "./harness/run/startup-runtime.js";
30
31
  import { streamHarnessRun } from "./harness/run/stream-run.js";
@@ -56,6 +57,8 @@ export class AgentHarnessRuntime {
56
57
  routingDefaultAgentId;
57
58
  threadMemorySync;
58
59
  unregisterThreadMemorySync;
60
+ mem0IngestionSync;
61
+ unregisterMem0IngestionSync;
59
62
  resolvedRuntimeAdapterOptions;
60
63
  healthMonitor;
61
64
  recoveryConfig;
@@ -139,6 +142,15 @@ export class AgentHarnessRuntime {
139
142
  this.threadMemorySync = null;
140
143
  this.unregisterThreadMemorySync = () => { };
141
144
  }
145
+ const mem0RuntimeConfig = readMem0RuntimeConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory, this.workspace.workspaceRoot);
146
+ if (mem0RuntimeConfig) {
147
+ this.mem0IngestionSync = new Mem0IngestionSync(this.persistence, mem0RuntimeConfig, runRoot);
148
+ this.unregisterMem0IngestionSync = this.eventBus.registerProjection(this.mem0IngestionSync);
149
+ }
150
+ else {
151
+ this.mem0IngestionSync = null;
152
+ this.unregisterMem0IngestionSync = () => { };
153
+ }
142
154
  this.recoveryConfig = getRecoveryConfig(workspace.refs);
143
155
  this.concurrencyConfig = getConcurrencyConfig(workspace.refs);
144
156
  const healthConfig = readHealthMonitorConfig(workspace);
@@ -568,8 +580,10 @@ export class AgentHarnessRuntime {
568
580
  this.closed = true;
569
581
  await this.healthMonitor?.stop();
570
582
  this.unregisterThreadMemorySync();
583
+ this.unregisterMem0IngestionSync();
571
584
  await Promise.allSettled(Array.from(this.backgroundTasks));
572
585
  await this.threadMemorySync?.close();
586
+ await this.mem0IngestionSync?.close();
573
587
  this.initialized = false;
574
588
  }
575
589
  async stop() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.128",
3
+ "version": "0.0.130",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -46,9 +46,12 @@
46
46
  "@libsql/client": "^0.17.0",
47
47
  "@llamaindex/ollama": "^0.1.23",
48
48
  "@modelcontextprotocol/sdk": "^1.12.0",
49
+ "@qdrant/js-client-rest": "^1.13.0",
50
+ "better-sqlite3": "^12.8.0",
49
51
  "deepagents": "1.8.4",
50
52
  "langchain": "^1.2.36",
51
53
  "llamaindex": "^0.12.1",
54
+ "mem0ai": "^2.4.4",
52
55
  "mustache": "^4.2.0",
53
56
  "yaml": "^2.8.1",
54
57
  "zod": "^3.25.67"