@botbotgo/agent-harness 0.0.127 → 0.0.129

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.
@@ -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
@@ -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.126";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.128";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.126";
1
+ export const AGENT_HARNESS_VERSION = "0.0.128";
@@ -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
+ }
@@ -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.127",
3
+ "version": "0.0.129",
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"