@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 +4 -0
- package/README.zh.md +4 -0
- package/dist/config/runtime/runtime-memory.yaml +8 -0
- package/dist/contracts/runtime.d.ts +19 -0
- package/dist/init-project.js +12 -2
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/runtime/harness/events/timeline.d.ts +2 -0
- package/dist/runtime/harness/events/timeline.js +52 -0
- package/dist/runtime/harness/index.d.ts +1 -0
- package/dist/runtime/harness/index.js +1 -0
- package/dist/runtime/harness/run/recovery.js +13 -11
- package/dist/runtime/harness/run/stream-run.js +15 -1
- package/dist/runtime/harness/system/mem0-ingestion-sync.d.ts +42 -0
- package/dist/runtime/harness/system/mem0-ingestion-sync.js +181 -0
- package/dist/runtime/harness/system/policy-engine.d.ts +5 -0
- package/dist/runtime/harness/system/policy-engine.js +5 -0
- package/dist/runtime/harness.d.ts +2 -0
- package/dist/runtime/harness.js +14 -0
- package/package.json +4 -1
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;
|
package/dist/init-project.js
CHANGED
|
@@ -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.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.129";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.129";
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/runtime/harness.js
CHANGED
|
@@ -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.
|
|
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"
|