@botbotgo/agent-harness 0.0.142 → 0.0.145

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.
Files changed (29) hide show
  1. package/dist/config/runtime/runtime-memory.yaml +5 -3
  2. package/dist/contracts/runtime.d.ts +14 -0
  3. package/dist/init-project.js +5 -3
  4. package/dist/package-version.d.ts +1 -1
  5. package/dist/package-version.js +1 -1
  6. package/dist/runtime/adapter/flow/invocation-flow.d.ts +2 -0
  7. package/dist/runtime/adapter/flow/stream-runtime.d.ts +2 -0
  8. package/dist/runtime/adapter/flow/stream-runtime.js +1 -1
  9. package/dist/runtime/adapter/invocation-result.d.ts +2 -1
  10. package/dist/runtime/adapter/invocation-result.js +2 -0
  11. package/dist/runtime/adapter/local-tool-invocation.js +12 -0
  12. package/dist/runtime/adapter/model/invocation-request.d.ts +1 -0
  13. package/dist/runtime/adapter/model/invocation-request.js +13 -3
  14. package/dist/runtime/adapter/model/message-assembly.d.ts +1 -1
  15. package/dist/runtime/adapter/model/message-assembly.js +12 -1
  16. package/dist/runtime/agent-runtime-adapter.d.ts +2 -0
  17. package/dist/runtime/harness/run/stream-run.d.ts +2 -0
  18. package/dist/runtime/harness/run/stream-run.js +1 -0
  19. package/dist/runtime/harness/system/runtime-memory-candidates.d.ts +3 -0
  20. package/dist/runtime/harness/system/runtime-memory-candidates.js +110 -0
  21. package/dist/runtime/harness/system/runtime-memory-policy.d.ts +18 -0
  22. package/dist/runtime/harness/system/runtime-memory-policy.js +73 -0
  23. package/dist/runtime/harness.d.ts +5 -0
  24. package/dist/runtime/harness.js +124 -2
  25. package/dist/tool-modules.d.ts +7 -0
  26. package/dist/tool-modules.js +1 -0
  27. package/dist/tools.d.ts +14 -0
  28. package/dist/workspace/object-loader.js +1 -0
  29. package/package.json +1 -1
@@ -28,9 +28,11 @@ spec:
28
28
 
29
29
  # agent-harness feature: stable namespace roots used by the runtime memory policy layer.
30
30
  namespaces:
31
- users: memories/users
32
- projects: memories/projects
33
- threads: memories/threads
31
+ users: memories/users/{userId}
32
+ projects: memories/projects/{projectId}
33
+ threads: memories/threads/{threadId}
34
+ agents: memories/agents/{agentId}
35
+ workspaces: memories/workspaces/{workspaceId}
34
36
 
35
37
  # agent-harness feature: retrieval defaults for selecting a bounded number of relevant memories per turn.
36
38
  retrieval:
@@ -97,6 +97,20 @@ export type RuntimeSnapshot = {
97
97
  skills: RuntimeSnapshotSkill[];
98
98
  memory: string[];
99
99
  };
100
+ export type MemoryCandidate = {
101
+ content: string;
102
+ summary?: string;
103
+ kind?: string;
104
+ scope?: string;
105
+ confidence?: number;
106
+ tags?: string[];
107
+ sourceType?: string;
108
+ sourceRef?: string;
109
+ observedAt?: string;
110
+ sensitivity?: string;
111
+ noStore?: boolean;
112
+ provenance?: Record<string, unknown>;
113
+ };
100
114
  /**
101
115
  * Operator-facing projection of tool execution policy already compiled into a binding.
102
116
  * This summarizes existing timeout, retry, validation, and retry-safety hints without
@@ -139,9 +139,11 @@ metadata:
139
139
  spec:
140
140
  enabled: true
141
141
  namespaces:
142
- users: memories/users
143
- projects: memories/projects
144
- threads: memories/threads
142
+ users: memories/users/{userId}
143
+ projects: memories/projects/{projectId}
144
+ threads: memories/threads/{threadId}
145
+ agents: memories/agents/{agentId}
146
+ workspaces: memories/workspaces/{workspaceId}
145
147
  retrieval:
146
148
  defaultTopK: 5
147
149
  maxPromptMemories: 8
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.141";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.144";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.141";
1
+ export const AGENT_HARNESS_VERSION = "0.0.144";
@@ -12,6 +12,7 @@ export declare function executeRuntimeInvocation(options: {
12
12
  context?: Record<string, unknown>;
13
13
  state?: Record<string, unknown>;
14
14
  files?: Record<string, unknown>;
15
+ memoryContext?: string;
15
16
  };
16
17
  resolveTools: (tools: CompiledTool[], binding?: CompiledAgentBinding) => unknown[];
17
18
  getToolNameMapping: (binding: CompiledAgentBinding) => ToolNameMapping;
@@ -19,6 +20,7 @@ export declare function executeRuntimeInvocation(options: {
19
20
  context?: Record<string, unknown>;
20
21
  state?: Record<string, unknown>;
21
22
  files?: Record<string, unknown>;
23
+ memoryContext?: string;
22
24
  }) => Promise<Map<string, ExecutableTool>>;
23
25
  callRuntimeWithToolParseRecovery: (request: unknown) => Promise<Record<string, unknown>>;
24
26
  }): Promise<RunResult>;
@@ -15,6 +15,7 @@ export declare function streamRuntimeExecution(options: {
15
15
  state?: Record<string, unknown>;
16
16
  files?: Record<string, unknown>;
17
17
  runId?: string;
18
+ memoryContext?: string;
18
19
  };
19
20
  primaryTools: CompiledTool[];
20
21
  toolNameMapping: ToolNameMapping;
@@ -33,6 +34,7 @@ export declare function streamRuntimeExecution(options: {
33
34
  context?: Record<string, unknown>;
34
35
  state?: Record<string, unknown>;
35
36
  files?: Record<string, unknown>;
37
+ memoryContext?: string;
36
38
  }) => Promise<{
37
39
  output: string;
38
40
  metadata?: Record<string, unknown>;
@@ -8,7 +8,7 @@ export async function* streamRuntimeExecution(options) {
8
8
  const request = buildInvocationRequest(options.binding, options.history, options.input, options.runtimeOptions);
9
9
  try {
10
10
  if (options.isLangChainBinding(options.binding) && options.canUseDirectModelStream && options.langChainStreamModel?.stream) {
11
- const stream = await options.withTimeout(() => options.langChainStreamModel.stream(buildRawModelMessages(options.binding, options.getSystemPrompt(options.binding), options.history, options.input)), computeRemainingTimeoutMs(options.streamDeadlineAt, options.invokeTimeoutMs), "model stream start", "stream");
11
+ const stream = await options.withTimeout(() => options.langChainStreamModel.stream(buildRawModelMessages(options.binding, options.getSystemPrompt(options.binding), options.history, options.input, options.runtimeOptions.memoryContext)), computeRemainingTimeoutMs(options.streamDeadlineAt, options.invokeTimeoutMs), "model stream start", "stream");
12
12
  let emitted = false;
13
13
  const projected = projectTextStreamChunks(options.iterateWithTimeout(stream, options.streamIdleTimeoutMs, "model stream", options.streamDeadlineAt, options.invokeTimeoutMs));
14
14
  let nextChunk = await projected.next();
@@ -1,8 +1,9 @@
1
- import type { RunResult } from "../../contracts/types.js";
1
+ import type { MemoryCandidate, RunResult } from "../../contracts/types.js";
2
2
  export type ExecutedToolResult = {
3
3
  toolName: string;
4
4
  output: unknown;
5
5
  isError?: boolean;
6
+ memoryCandidates?: MemoryCandidate[];
6
7
  };
7
8
  export declare function finalizeInvocationResult(params: {
8
9
  bindingAgentId: string;
@@ -18,6 +18,7 @@ export function finalizeInvocationResult(params) {
18
18
  const structuredResponse = result.structuredResponse;
19
19
  const files = asRecord(result.files);
20
20
  const stateSnapshot = buildStateSnapshot(result);
21
+ const memoryCandidates = executedToolResults.flatMap((toolResult) => toolResult.memoryCandidates ?? []);
21
22
  return {
22
23
  threadId,
23
24
  runId,
@@ -31,6 +32,7 @@ export function finalizeInvocationResult(params) {
31
32
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
32
33
  metadata: {
33
34
  ...(executedToolResults.length > 0 ? { executedToolResults } : {}),
35
+ ...(memoryCandidates.length > 0 ? { memoryCandidates } : {}),
34
36
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
35
37
  ...(outputContent !== undefined ? { outputContent } : {}),
36
38
  ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
@@ -2,6 +2,7 @@ import { ToolMessage } from "@langchain/core/messages";
2
2
  import { createModelFacingToolNameLookupCandidates, resolveModelFacingToolName } from "./tool/tool-name-mapping.js";
3
3
  import { canReplayToolCallsLocally } from "./tool/tool-replay.js";
4
4
  import { extractToolCallsFromResult, normalizeToolArgsForSchema, stringifyToolOutput } from "./tool/tool-arguments.js";
5
+ import { extractMemoryCandidatesFromToolOutput } from "../harness/system/runtime-memory-candidates.js";
5
6
  export async function runLocalToolInvocationLoop({ binding, request, primaryTools, toolNameMapping, executableTools, builtinExecutableTools, callRuntimeWithToolParseRecovery, }) {
6
7
  const executedToolResults = [];
7
8
  let activeRequest = request;
@@ -9,6 +10,14 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
9
10
  const maxToolIterations = 8;
10
11
  let pendingResult;
11
12
  let result;
13
+ const toolCatalog = new Map();
14
+ for (const tool of primaryTools) {
15
+ toolCatalog.set(tool.name, tool);
16
+ const modelFacingName = toolNameMapping.originalToModelFacing.get(tool.name);
17
+ if (modelFacingName) {
18
+ toolCatalog.set(modelFacingName, tool);
19
+ }
20
+ }
12
21
  for (let iteration = 0; iteration < maxToolIterations; iteration += 1) {
13
22
  result = pendingResult ?? await callRuntimeWithToolParseRecovery(activeRequest);
14
23
  pendingResult = undefined;
@@ -39,11 +48,14 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
39
48
  if (!activeExecutable) {
40
49
  throw new Error(`Tool ${toolCall.name} is not configured for this agent.`);
41
50
  }
51
+ const compiledTool = toolCatalog.get(toolCall.name) ?? toolCatalog.get(resolvedToolName);
42
52
  const normalizedArgs = normalizeToolArgsForSchema(toolCall.args, activeExecutable.schema);
43
53
  const toolResult = await activeExecutable.invoke(normalizedArgs);
54
+ const memoryCandidates = compiledTool ? extractMemoryCandidatesFromToolOutput(compiledTool, toolResult) : [];
44
55
  executedToolResults.push({
45
56
  toolName: activeExecutable.name,
46
57
  output: toolResult,
58
+ ...(memoryCandidates.length > 0 ? { memoryCandidates } : {}),
47
59
  });
48
60
  nextMessages.push(new ToolMessage({
49
61
  name: activeExecutable.name,
@@ -7,4 +7,5 @@ export declare function buildSlashCommandSkillInstruction(binding: CompiledAgent
7
7
  export declare function buildInvocationRequest(binding: CompiledAgentBinding, history: TranscriptMessage[], input: MessageContent, options?: {
8
8
  state?: Record<string, unknown>;
9
9
  files?: Record<string, unknown>;
10
+ memoryContext?: string;
10
11
  }): Record<string, unknown>;
@@ -37,11 +37,21 @@ export function buildSlashCommandSkillInstruction(binding, input) {
37
37
  export function buildInvocationRequest(binding, history, input, options = {}) {
38
38
  const userInvocableInstruction = buildSlashCommandSkillInstruction(binding, input);
39
39
  const messages = buildAgentMessages(history, input);
40
+ const memoryInstruction = typeof options.memoryContext === "string" && options.memoryContext.trim().length > 0
41
+ ? [
42
+ "Relevant durable memory was retrieved for this run.",
43
+ "Use it when it is still applicable, but prefer fresher direct evidence if there is any conflict.",
44
+ "",
45
+ options.memoryContext.trim(),
46
+ ].join("\n")
47
+ : undefined;
40
48
  return {
41
49
  ...(options.state ?? {}),
42
50
  ...(options.files ? { files: options.files } : {}),
43
- messages: userInvocableInstruction
44
- ? [{ role: "system", content: userInvocableInstruction }, ...messages]
45
- : messages,
51
+ messages: [
52
+ ...(memoryInstruction ? [{ role: "system", content: memoryInstruction }] : []),
53
+ ...(userInvocableInstruction ? [{ role: "system", content: userInvocableInstruction }] : []),
54
+ ...messages,
55
+ ],
46
56
  };
47
57
  }
@@ -1,6 +1,6 @@
1
1
  import type { CompiledAgentBinding, MessageContent, TranscriptMessage } from "../../../contracts/types.js";
2
2
  export declare function buildStateSnapshot(result: Record<string, unknown>): Record<string, unknown> | undefined;
3
- export declare function buildRawModelMessages(binding: CompiledAgentBinding, systemPrompt: string | undefined, history: TranscriptMessage[], input: MessageContent): Array<{
3
+ export declare function buildRawModelMessages(binding: CompiledAgentBinding, systemPrompt: string | undefined, history: TranscriptMessage[], input: MessageContent, memoryContext?: string): Array<{
4
4
  role: string;
5
5
  content: MessageContent;
6
6
  }>;
@@ -7,11 +7,22 @@ export function buildStateSnapshot(result) {
7
7
  delete snapshot.files;
8
8
  return Object.keys(snapshot).length > 0 ? snapshot : undefined;
9
9
  }
10
- export function buildRawModelMessages(binding, systemPrompt, history, input) {
10
+ export function buildRawModelMessages(binding, systemPrompt, history, input, memoryContext) {
11
11
  const messages = [];
12
12
  if (systemPrompt) {
13
13
  messages.push({ role: "system", content: systemPrompt });
14
14
  }
15
+ if (memoryContext?.trim()) {
16
+ messages.push({
17
+ role: "system",
18
+ content: [
19
+ "Relevant durable memory was retrieved for this run.",
20
+ "Use it when it still applies, but prefer fresher direct evidence if there is any conflict.",
21
+ "",
22
+ memoryContext.trim(),
23
+ ].join("\n"),
24
+ });
25
+ }
15
26
  const userInvocableInstruction = buildSlashCommandSkillInstruction(binding, input);
16
27
  if (userInvocableInstruction) {
17
28
  messages.push({ role: "system", content: userInvocableInstruction });
@@ -80,12 +80,14 @@ export declare class AgentRuntimeAdapter {
80
80
  context?: Record<string, unknown>;
81
81
  state?: Record<string, unknown>;
82
82
  files?: Record<string, unknown>;
83
+ memoryContext?: string;
83
84
  }): Promise<RunResult>;
84
85
  stream(binding: CompiledAgentBinding, input: MessageContent, threadId: string, history?: TranscriptMessage[], options?: {
85
86
  context?: Record<string, unknown>;
86
87
  state?: Record<string, unknown>;
87
88
  files?: Record<string, unknown>;
88
89
  runId?: string;
90
+ memoryContext?: string;
89
91
  }): AsyncGenerator<RuntimeStreamChunk | string>;
90
92
  }
91
93
  export { AgentRuntimeAdapter as RuntimeAdapter, AGENT_INTERRUPT_SENTINEL_PREFIX, AGENT_INTERRUPT_SENTINEL_PREFIX as INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError, };
@@ -15,6 +15,7 @@ type StreamRunOptions = {
15
15
  context?: Record<string, unknown>;
16
16
  state?: Record<string, unknown>;
17
17
  files?: Record<string, unknown>;
18
+ memoryContext?: string;
18
19
  };
19
20
  threadId: string;
20
21
  runId: string;
@@ -28,6 +29,7 @@ type StreamRunOptions = {
28
29
  state?: Record<string, unknown>;
29
30
  files?: Record<string, unknown>;
30
31
  runId: string;
32
+ memoryContext?: string;
31
33
  }) => AsyncIterable<RuntimeStreamChunk>;
32
34
  invokeWithHistory: (binding: CompiledAgentBinding, input: MessageContent, threadId: string, runId: string) => Promise<RunResult>;
33
35
  emit: (threadId: string, runId: string, sequence: number, eventType: string, payload: Record<string, unknown>) => Promise<HarnessEvent>;
@@ -60,6 +60,7 @@ export async function* streamHarnessRun(options) {
60
60
  state: options.invocation.state,
61
61
  files: options.invocation.files,
62
62
  runId: options.runId,
63
+ memoryContext: options.invocation.memoryContext,
63
64
  })) {
64
65
  if (!rawChunk) {
65
66
  continue;
@@ -0,0 +1,3 @@
1
+ import type { CompiledTool, MemoryCandidate } from "../../../contracts/types.js";
2
+ export declare function extractMemoryCandidatesFromToolOutput(tool: CompiledTool, output: unknown): MemoryCandidate[];
3
+ export declare function renderMemoryCandidatesMarkdown(title: string, candidates: MemoryCandidate[]): string;
@@ -0,0 +1,110 @@
1
+ function asRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
3
+ }
4
+ function asString(value) {
5
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
6
+ }
7
+ function asNumber(value) {
8
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
9
+ }
10
+ function asStringArray(value) {
11
+ if (!Array.isArray(value)) {
12
+ return undefined;
13
+ }
14
+ const items = value
15
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
16
+ .map((item) => item.trim());
17
+ return items.length > 0 ? items : undefined;
18
+ }
19
+ function stringifyCandidateContent(output) {
20
+ if (typeof output === "string") {
21
+ return output.trim();
22
+ }
23
+ if (typeof output === "number" || typeof output === "boolean") {
24
+ return String(output);
25
+ }
26
+ if (typeof output === "object" && output !== null) {
27
+ try {
28
+ return JSON.stringify(output, null, 2);
29
+ }
30
+ catch {
31
+ return "";
32
+ }
33
+ }
34
+ return "";
35
+ }
36
+ function normalizeCandidate(value, fallback) {
37
+ const record = asRecord(value);
38
+ if (!record) {
39
+ return null;
40
+ }
41
+ const content = asString(record.content);
42
+ if (!content) {
43
+ return null;
44
+ }
45
+ return {
46
+ content,
47
+ ...(asString(record.summary) ? { summary: asString(record.summary) } : {}),
48
+ ...(asString(record.kind) ?? fallback.kind ? { kind: asString(record.kind) ?? fallback.kind } : {}),
49
+ ...(asString(record.scope) ?? fallback.scope ? { scope: asString(record.scope) ?? fallback.scope } : {}),
50
+ ...(asNumber(record.confidence) !== undefined ? { confidence: asNumber(record.confidence) } : {}),
51
+ ...(asStringArray(record.tags) ?? fallback.tags ? { tags: asStringArray(record.tags) ?? fallback.tags } : {}),
52
+ ...(asString(record.sourceType) ?? fallback.sourceType ? { sourceType: asString(record.sourceType) ?? fallback.sourceType } : {}),
53
+ ...(asString(record.sourceRef) ? { sourceRef: asString(record.sourceRef) } : {}),
54
+ ...(asString(record.observedAt) ? { observedAt: asString(record.observedAt) } : {}),
55
+ ...(asString(record.sensitivity) ? { sensitivity: asString(record.sensitivity) } : {}),
56
+ ...(typeof record.noStore === "boolean" ? { noStore: record.noStore } : {}),
57
+ ...(asRecord(record.provenance) ? { provenance: asRecord(record.provenance) } : {}),
58
+ };
59
+ }
60
+ export function extractMemoryCandidatesFromToolOutput(tool, output) {
61
+ const memoryConfig = asRecord(tool.config?.memory);
62
+ if (memoryConfig?.enabled !== true) {
63
+ return [];
64
+ }
65
+ const fallback = {
66
+ sourceType: "tool-output",
67
+ kind: asString(memoryConfig.kind),
68
+ scope: asString(memoryConfig.scope) ?? "thread",
69
+ tags: asStringArray(memoryConfig.tags),
70
+ };
71
+ const explicitCandidates = asRecord(output)?.memoryCandidates;
72
+ if (Array.isArray(explicitCandidates)) {
73
+ const maxCandidates = asNumber(memoryConfig.maxCandidates) ?? explicitCandidates.length;
74
+ return explicitCandidates
75
+ .map((candidate) => normalizeCandidate(candidate, fallback))
76
+ .filter((candidate) => Boolean(candidate))
77
+ .slice(0, maxCandidates);
78
+ }
79
+ const content = stringifyCandidateContent(output);
80
+ if (!content) {
81
+ return [];
82
+ }
83
+ return [{
84
+ content,
85
+ ...(fallback.kind ? { kind: fallback.kind } : {}),
86
+ ...(fallback.scope ? { scope: fallback.scope } : {}),
87
+ ...(fallback.tags ? { tags: fallback.tags } : {}),
88
+ sourceType: fallback.sourceType,
89
+ }];
90
+ }
91
+ export function renderMemoryCandidatesMarkdown(title, candidates) {
92
+ const lines = [`# ${title}`, ""];
93
+ if (candidates.length === 0) {
94
+ lines.push("(none)", "");
95
+ return lines.join("\n");
96
+ }
97
+ for (const candidate of candidates) {
98
+ lines.push(`## ${(candidate.summary ?? candidate.content).split("\n")[0].slice(0, 120)}`);
99
+ lines.push(`- kind: ${candidate.kind ?? "summary"}`);
100
+ lines.push(`- scope: ${candidate.scope ?? "thread"}`);
101
+ lines.push(`- source_type: ${candidate.sourceType ?? "tool-output"}`);
102
+ if (candidate.tags && candidate.tags.length > 0) {
103
+ lines.push(`- tags: ${candidate.tags.join(", ")}`);
104
+ }
105
+ lines.push("");
106
+ lines.push(candidate.summary ?? candidate.content);
107
+ lines.push("");
108
+ }
109
+ return lines.join("\n");
110
+ }
@@ -0,0 +1,18 @@
1
+ export type ResolvedRuntimeMemoryPolicyConfig = {
2
+ enabled: true;
3
+ retrieval: {
4
+ defaultTopK: number;
5
+ maxPromptMemories: number;
6
+ };
7
+ namespaces: {
8
+ thread: string;
9
+ workspace: string;
10
+ agent: string;
11
+ user: string;
12
+ project: string;
13
+ };
14
+ };
15
+ export declare function normalizeLangMemMemoryKind(kind: string | undefined): "semantic" | "episodic" | "procedural";
16
+ export declare function readRuntimeMemoryPolicyConfig(runtimeMemory: Record<string, unknown> | undefined, workspaceRoot: string): ResolvedRuntimeMemoryPolicyConfig | undefined;
17
+ export declare function resolveMemoryNamespace(template: string, values: Record<string, string | undefined>): string[];
18
+ export declare function scoreMemoryText(query: string, content: string, scopeBoost?: number): number;
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ function asRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
4
+ }
5
+ function asString(value) {
6
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
7
+ }
8
+ function asPositiveInteger(value) {
9
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
10
+ }
11
+ export function normalizeLangMemMemoryKind(kind) {
12
+ const normalized = kind?.trim().toLowerCase();
13
+ if (!normalized) {
14
+ return "semantic";
15
+ }
16
+ if (["episode", "episodic", "summary", "experience", "reflection"].includes(normalized)) {
17
+ return "episodic";
18
+ }
19
+ if (["procedural", "workflow", "procedure", "runbook", "playbook"].includes(normalized)) {
20
+ return "procedural";
21
+ }
22
+ return "semantic";
23
+ }
24
+ export function readRuntimeMemoryPolicyConfig(runtimeMemory, workspaceRoot) {
25
+ if (runtimeMemory?.enabled !== true) {
26
+ return undefined;
27
+ }
28
+ const retrieval = asRecord(runtimeMemory.retrieval);
29
+ const namespaces = asRecord(runtimeMemory.namespaces);
30
+ const workspaceId = path.basename(workspaceRoot) || "default";
31
+ return {
32
+ enabled: true,
33
+ retrieval: {
34
+ defaultTopK: asPositiveInteger(retrieval?.defaultTopK) ?? 5,
35
+ maxPromptMemories: asPositiveInteger(retrieval?.maxPromptMemories) ?? 8,
36
+ },
37
+ namespaces: {
38
+ thread: asString(namespaces?.threads) ?? "memories/threads/{threadId}",
39
+ workspace: asString(namespaces?.workspaces) ?? `memories/workspaces/${workspaceId}`,
40
+ agent: asString(namespaces?.agents) ?? "memories/agents/{agentId}",
41
+ user: asString(namespaces?.users) ?? "memories/users/{userId}",
42
+ project: asString(namespaces?.projects) ?? "memories/projects/{projectId}",
43
+ },
44
+ };
45
+ }
46
+ export function resolveMemoryNamespace(template, values) {
47
+ const rendered = template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => values[key] ?? key);
48
+ return rendered
49
+ .split("/")
50
+ .map((part) => part.trim())
51
+ .filter((part) => part.length > 0);
52
+ }
53
+ function tokenize(text) {
54
+ return text
55
+ .toLowerCase()
56
+ .split(/[^a-z0-9_]+/i)
57
+ .map((token) => token.trim())
58
+ .filter((token) => token.length > 2);
59
+ }
60
+ export function scoreMemoryText(query, content, scopeBoost = 0) {
61
+ const queryTokens = new Set(tokenize(query));
62
+ if (queryTokens.size === 0) {
63
+ return scopeBoost;
64
+ }
65
+ const contentTokens = tokenize(content);
66
+ let overlap = 0;
67
+ for (const token of contentTokens) {
68
+ if (queryTokens.has(token)) {
69
+ overlap += 1;
70
+ }
71
+ }
72
+ return overlap + scopeBoost;
73
+ }
@@ -19,6 +19,7 @@ export declare class AgentHarnessRuntime {
19
19
  private readonly defaultRunRootValue;
20
20
  private readonly defaultStore;
21
21
  private readonly runtimeMemoryStore;
22
+ private readonly runtimeMemoryPolicy;
22
23
  private readonly routingRules;
23
24
  private readonly routingDefaultAgentId?;
24
25
  private readonly threadMemorySync;
@@ -90,6 +91,10 @@ export declare class AgentHarnessRuntime {
90
91
  private getRunCancellation;
91
92
  private finalizeCancelledRun;
92
93
  private invokeWithHistory;
94
+ private resolveMemoryNamespace;
95
+ private buildRuntimeMemoryContext;
96
+ private persistRuntimeMemoryCandidates;
97
+ private appendMemoryDigest;
93
98
  private resolvePersistedRunPriority;
94
99
  private enqueuePendingRunSlot;
95
100
  private executeQueuedRun;
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { SqlitePersistence } from "../persistence/sqlite-store.js";
2
3
  import { createPersistentId } from "../utils/id.js";
3
4
  import { AgentRuntimeAdapter } from "./agent-runtime-adapter.js";
@@ -28,6 +29,8 @@ import { bindingSupportsRunningReplay, getWorkspaceBinding, resolveWorkspaceAgen
28
29
  import { describeWorkspaceInventory, getAgentInventoryRecord, listAgentSkills as listWorkspaceAgentSkills, } from "./harness/system/inventory.js";
29
30
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
30
31
  import { Mem0IngestionSync, readMem0RuntimeConfig } from "./harness/system/mem0-ingestion-sync.js";
32
+ import { renderMemoryCandidatesMarkdown } from "./harness/system/runtime-memory-candidates.js";
33
+ import { normalizeLangMemMemoryKind, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, scoreMemoryText, } from "./harness/system/runtime-memory-policy.js";
31
34
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
32
35
  import { initializeHarnessRuntime, reclaimExpiredClaimedRuns as reclaimHarnessExpiredClaimedRuns, recoverStartupRuns as recoverHarnessStartupRuns, isStaleRunningRun as isHarnessStaleRunningRun, } from "./harness/run/startup-runtime.js";
33
36
  import { streamHarnessRun } from "./harness/run/stream-run.js";
@@ -55,6 +58,7 @@ export class AgentHarnessRuntime {
55
58
  defaultRunRootValue;
56
59
  defaultStore;
57
60
  runtimeMemoryStore;
61
+ runtimeMemoryPolicy;
58
62
  routingRules;
59
63
  routingDefaultAgentId;
60
64
  threadMemorySync;
@@ -118,6 +122,7 @@ export class AgentHarnessRuntime {
118
122
  ? this.defaultRuntimeEntryBinding.harnessRuntime.runtimeMemory.store
119
123
  : undefined;
120
124
  this.runtimeMemoryStore = resolveStoreFromConfig(this.stores, runtimeMemoryStoreConfig, runRoot) ?? this.defaultStore;
125
+ this.runtimeMemoryPolicy = readRuntimeMemoryPolicyConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory, this.workspace.workspaceRoot) ?? null;
121
126
  this.resolvedRuntimeAdapterOptions = resolveRuntimeAdapterOptions({
122
127
  workspace,
123
128
  runtimeAdapterOptions,
@@ -349,9 +354,14 @@ export class AgentHarnessRuntime {
349
354
  }
350
355
  async invokeWithHistory(binding, input, threadId, runId, resumePayload, priorHistory, options = {}) {
351
356
  const history = priorHistory ?? await this.loadPriorHistory(threadId, runId);
357
+ const memoryContext = options.memoryContext ?? await this.buildRuntimeMemoryContext(binding, threadId, input);
352
358
  const startedAt = Date.now();
353
359
  try {
354
- const result = await this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, history, options);
360
+ const result = await this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, history, {
361
+ ...options,
362
+ ...(memoryContext ? { memoryContext } : {}),
363
+ });
364
+ await this.persistRuntimeMemoryCandidates(binding, threadId, runId, result.metadata?.memoryCandidates);
355
365
  this.recordLlmSuccess(startedAt);
356
366
  return result;
357
367
  }
@@ -360,6 +370,115 @@ export class AgentHarnessRuntime {
360
370
  throw error;
361
371
  }
362
372
  }
373
+ resolveMemoryNamespace(scope, binding, options = {}) {
374
+ const workspaceRoot = binding.harnessRuntime.workspaceRoot ?? this.workspace.workspaceRoot;
375
+ const workspaceId = path.basename(workspaceRoot) || "default";
376
+ const template = this.runtimeMemoryPolicy?.namespaces[scope] ?? `memories/${scope}s/{${scope}Id}`;
377
+ return resolveMemoryNamespace(template, {
378
+ threadId: options.threadId,
379
+ agentId: binding.agent.id,
380
+ workspaceId,
381
+ userId: options.userId ?? "default",
382
+ projectId: options.projectId ?? workspaceId,
383
+ });
384
+ }
385
+ async buildRuntimeMemoryContext(binding, threadId, input) {
386
+ const threadNamespace = this.resolveMemoryNamespace("thread", binding, { threadId });
387
+ const agentNamespace = this.resolveMemoryNamespace("agent", binding);
388
+ const workspaceNamespace = this.resolveMemoryNamespace("workspace", binding);
389
+ const docs = await Promise.all([
390
+ this.runtimeMemoryStore.get(threadNamespace, "durable-summary.md"),
391
+ this.runtimeMemoryStore.get(threadNamespace, "tool-memory.md"),
392
+ this.runtimeMemoryStore.get(threadNamespace, "semantic.md"),
393
+ this.runtimeMemoryStore.get(threadNamespace, "episodic.md"),
394
+ this.runtimeMemoryStore.get(threadNamespace, "procedural.md"),
395
+ this.runtimeMemoryStore.get(agentNamespace, "procedural.md"),
396
+ this.runtimeMemoryStore.get(workspaceNamespace, "semantic.md"),
397
+ this.runtimeMemoryStore.get(workspaceNamespace, "procedural.md"),
398
+ ]);
399
+ const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
400
+ const ranked = docs
401
+ .map((doc, index) => {
402
+ const content = doc?.value?.content;
403
+ if (typeof content !== "string" || content.trim().length === 0) {
404
+ return null;
405
+ }
406
+ const scopeBoost = index < 5 ? 4 : index === 5 ? 2 : 1;
407
+ return {
408
+ content: content.trim(),
409
+ score: scoreMemoryText(query, content, scopeBoost),
410
+ };
411
+ })
412
+ .filter((value) => Boolean(value))
413
+ .sort((left, right) => right.score - left.score)
414
+ .slice(0, this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8);
415
+ if (ranked.length === 0) {
416
+ return undefined;
417
+ }
418
+ return ranked.map((entry) => entry.content).join("\n\n");
419
+ }
420
+ async persistRuntimeMemoryCandidates(binding, threadId, runId, value) {
421
+ if (!Array.isArray(value) || value.length === 0) {
422
+ return;
423
+ }
424
+ const candidates = value
425
+ .filter((candidate) => typeof candidate === "object" && candidate !== null)
426
+ .filter((candidate) => candidate.noStore !== true && typeof candidate.content === "string" && candidate.content.trim().length > 0);
427
+ if (candidates.length === 0) {
428
+ return;
429
+ }
430
+ const threadCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
431
+ const workspaceCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
432
+ const agentCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
433
+ await this.runtimeMemoryStore.put(["memories", "candidates", threadId], `${runId}.json`, {
434
+ runId,
435
+ threadId,
436
+ storedAt: new Date().toISOString(),
437
+ candidates,
438
+ });
439
+ const writes = [];
440
+ if (threadCandidates.length > 0) {
441
+ writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
442
+ }
443
+ if (workspaceCandidates.length > 0) {
444
+ writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("workspace", binding), "tool-memory.md", workspaceCandidates, 20, "Workspace Tool Memory"));
445
+ }
446
+ if (agentCandidates.length > 0) {
447
+ writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("agent", binding), "tool-memory.md", agentCandidates, 20, "Agent Tool Memory"));
448
+ }
449
+ await Promise.all(writes);
450
+ }
451
+ async appendMemoryDigest(namespace, key, candidates, maxEntries, title) {
452
+ const existing = await this.runtimeMemoryStore.get(namespace, key);
453
+ const existingItems = Array.isArray(existing?.value?.items)
454
+ ? ((existing?.value).items ?? [])
455
+ : [];
456
+ const merged = [...existingItems];
457
+ for (const candidate of candidates) {
458
+ if (merged.some((entry) => entry.content === candidate.content && (entry.scope ?? "thread") === (candidate.scope ?? "thread"))) {
459
+ continue;
460
+ }
461
+ merged.push(candidate);
462
+ }
463
+ const recent = merged.slice(-maxEntries);
464
+ const taxonomyGroups = new Map();
465
+ for (const candidate of recent) {
466
+ const kind = normalizeLangMemMemoryKind(candidate.kind);
467
+ const existing = taxonomyGroups.get(kind) ?? [];
468
+ existing.push({ ...candidate, kind });
469
+ taxonomyGroups.set(kind, existing);
470
+ }
471
+ await this.runtimeMemoryStore.put(namespace, key, {
472
+ content: `${renderMemoryCandidatesMarkdown(title, recent)}\n`,
473
+ items: recent,
474
+ });
475
+ await Promise.all(Array.from(taxonomyGroups.entries()).map(async ([kind, items]) => {
476
+ await this.runtimeMemoryStore.put(namespace, `${kind}.md`, {
477
+ content: `${renderMemoryCandidatesMarkdown(`${title} (${kind})`, items)}\n`,
478
+ items,
479
+ });
480
+ }));
481
+ }
363
482
  async resolvePersistedRunPriority(threadId, runId) {
364
483
  const persisted = await this.persistence.getRunRequest(threadId, runId);
365
484
  return normalizeRunPriority(persisted?.priority);
@@ -514,7 +633,10 @@ export class AgentHarnessRuntime {
514
633
  const stream = streamHarnessRun({
515
634
  binding,
516
635
  input: options.input,
517
- invocation,
636
+ invocation: {
637
+ ...invocation,
638
+ memoryContext: await this.buildRuntimeMemoryContext(binding, threadId, options.input),
639
+ },
518
640
  threadId,
519
641
  runId,
520
642
  selectedAgentId,
@@ -6,6 +6,13 @@ export type LoadedToolModule = {
6
6
  schema: ReturnType<typeof normalizeToolSchema>;
7
7
  description: string;
8
8
  retryable?: boolean;
9
+ memory?: {
10
+ enabled: boolean;
11
+ kind?: string;
12
+ scope?: string;
13
+ maxCandidates?: number;
14
+ tags?: string[];
15
+ };
9
16
  };
10
17
  export type LoadedSkillModule = {
11
18
  name: string;
@@ -22,6 +22,7 @@ function loadToolObjectDefinition(imported, exportName) {
22
22
  schema: normalizeToolSchema(definition.schema),
23
23
  description: definition.description.trim(),
24
24
  retryable: definition.retryable === true,
25
+ ...(definition.memory ? { memory: { ...definition.memory } } : {}),
25
26
  };
26
27
  }
27
28
  export function isSupportedToolModulePath(filePath) {
package/dist/tools.d.ts CHANGED
@@ -6,6 +6,13 @@ export type ToolDefinitionObject = {
6
6
  description: string;
7
7
  schema: SchemaInput;
8
8
  retryable?: boolean;
9
+ memory?: {
10
+ enabled: boolean;
11
+ kind?: string;
12
+ scope?: string;
13
+ maxCandidates?: number;
14
+ tags?: string[];
15
+ };
9
16
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
10
17
  [TOOL_DEFINITION_MARKER]: true;
11
18
  };
@@ -17,6 +24,13 @@ export declare function tool(definition: {
17
24
  description: string;
18
25
  schema: SchemaInput;
19
26
  retryable?: boolean;
27
+ memory?: {
28
+ enabled: boolean;
29
+ kind?: string;
30
+ scope?: string;
31
+ maxCandidates?: number;
32
+ tags?: string[];
33
+ };
20
34
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
21
35
  }): ToolDefinitionObject;
22
36
  export {};
@@ -762,6 +762,7 @@ export async function readToolModuleItems(root) {
762
762
  description: definition.description,
763
763
  implementationName: definition.implementationName,
764
764
  ...(definition.retryable !== undefined ? { retryable: definition.retryable } : {}),
765
+ ...(definition.memory ? { config: { memory: definition.memory } } : {}),
765
766
  },
766
767
  sourcePath: filePath,
767
768
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.142",
3
+ "version": "0.0.145",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",