@botbotgo/agent-harness 0.0.306 → 0.0.308

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
@@ -469,9 +469,9 @@ import { AgentHarnessRuntime, createAgentHarness } from "@botbotgo/agent-harness
469
469
  const runtime: AgentHarnessRuntime = await createAgentHarness("/absolute/path/to/workspace");
470
470
  ```
471
471
 
472
- `createAgentHarness(...)` loads the workspace, resolves `resources/`, initializes persistence under `runtimeRoot`, and starts runtime maintenance.
472
+ `createAgentHarness(...)` loads the workspace, resolves workspace sources, initializes persistence under `runtimeRoot`, and starts runtime maintenance.
473
473
 
474
- `runtime.spec.resources` may attach multiple extra resource packages. Each entry may point either to a package root that contains `resources/`, or directly to a resource folder that contains its own `package.json`.
474
+ `runtime.spec.sources` is the primary public discovery surface for local tools, package tools, and skill packages.
475
475
 
476
476
  ```yaml
477
477
  apiVersion: agent-harness/v1alpha1
@@ -479,11 +479,29 @@ kind: Runtime
479
479
  metadata:
480
480
  name: default
481
481
  spec:
482
- resources:
483
- - ../shared-tools
484
- - file:../shared-skills
482
+ sources:
483
+ tools:
484
+ - file://./resources/tools
485
+ - file://../shared-tools
486
+ - npm://@acme/agent-tools
487
+ skills:
488
+ - file://./resources/skills
489
+ - https://example.com/skills/review/SKILL.md
485
490
  ```
486
491
 
492
+ Tool-source rules:
493
+
494
+ - `file://...` scans only the configured folder
495
+ - `npm://...` resolves one package, auto-installs it when missing, and discovers exported `tool({...})` definitions from the package entry
496
+ - tool discovery never traverses `node_modules/**`
497
+
498
+ Skill-source rules:
499
+
500
+ - `file://...` accepts a skill collection folder, a single skill root, or a direct `SKILL.md` path
501
+ - `http://...` and `https://...` currently accept a single remote `SKILL.md`
502
+
503
+ `runtime.spec.resources` remains supported as a compatibility path for attached resource packages.
504
+
487
505
  `createAgentHarness(..., { load })` accepts workspace loading controls.
488
506
 
489
507
  Merge order is deterministic:
@@ -739,8 +757,10 @@ Discovery rules:
739
757
 
740
758
  - every YAML document under `config/**` is discovered recursively; filenames and subfolders are organizational only
741
759
  - YAML object semantics come from `kind`, `metadata.name` or `id`, and object content rather than the file path
742
- - local tools are auto-discovered from `resources/tools/**/*.js|*.mjs|*.cjs` when they export `tool({...})`
743
- - skills are auto-discovered from `resources/skills/**/SKILL.md`
760
+ - `Runtime.spec.sources.tools` defaults to `file://./resources/tools`
761
+ - `Runtime.spec.sources.skills` defaults to `file://./resources/skills`
762
+ - local file-based tools are auto-discovered from each configured tool folder when modules export `tool({...})`
763
+ - file-based skills are auto-discovered from each configured skill source
744
764
  - a minimal workspace can start with only `config/models.yaml`; the repository defaults provide the `Runtime`, the default `orchestra` host, and runtime-managed durable memory with `enabled: true`
745
765
  - when you do not override runtime placement, harness-owned generated state is written under `./.botbotgo/`
746
766
 
@@ -768,7 +788,9 @@ For DeepAgents-backed agents, the runtime still keeps an internal compatibility
768
788
 
769
789
  Default wiring guidance:
770
790
 
771
- - let workspace startup scan local and attached `resources` packages into one registry
791
+ - let `Runtime.spec.sources` declare the tool and skill roots the workspace owns
792
+ - let workspace startup scan only those declared sources into one registry
793
+ - let workspace startup scan local and attached `resources` packages into one registry when compatibility paths are still in use
772
794
  - let agents whitelist tools and skills by name
773
795
  - keep `config/catalogs/tools.yaml` for reusable shared tools
774
796
  - keep `config/catalogs/mcp.yaml` for shared MCP server definitions
@@ -810,6 +832,8 @@ Important fields:
810
832
 
811
833
  - `runtimeRoot`
812
834
  - `concurrency.maxConcurrentRequests`
835
+ - `sources.tools`
836
+ - `sources.skills`
813
837
  - `routing.defaultAgentId`
814
838
  - `routing.rules`
815
839
  - `toolModuleDiscovery.scope`
@@ -826,7 +850,17 @@ Important fields:
826
850
  - `maintenance.checkpoints` trims backend checkpoint state used for resume/recovery
827
851
  - `maintenance.records` trims harness-owned terminal session/request records stored in `runtime.sqlite`
828
852
 
829
- `toolModuleDiscovery.scope` controls how local `resources/tools/`-style module discovery walks tool directories:
853
+ `sources.tools` controls which tool roots or packages participate in workspace discovery:
854
+
855
+ - `file://...` for folder scanning
856
+ - `npm://...` for package-entry discovery and auto-install when missing
857
+
858
+ `sources.skills` controls which skill folders or skill documents participate in workspace discovery:
859
+
860
+ - `file://...` for local folders, skill roots, or direct `SKILL.md`
861
+ - `http://...` / `https://...` for one remote `SKILL.md`
862
+
863
+ `toolModuleDiscovery.scope` controls how local `resources/tools/`-style file discovery walks tool directories:
830
864
 
831
865
  - `recursive` is the default and keeps scanning nested folders
832
866
  - `top-level` limits module discovery to files directly under each tool root while leaving YAML catalogs recursive
@@ -942,6 +976,8 @@ The default repository shape uses:
942
976
  - `KnowledgeRuntime`: hot path + background formation + long-term maintenance
943
977
  - `ProceduralMemoryRuntime`: background formation + scheduled or idle maintenance
944
978
 
979
+ In the shipped runtime, explicit durable facts such as “remember I moved to the United States” still go to `KnowledgeRuntime` and land in `knowledge/knowledge.sqlite`. Background procedural learning writes its own store and state files under the same data root, such as `knowledge/procedural-memory.sqlite` and `knowledge/procedural-memory-state.json`.
980
+
945
981
  For DeepAgents-backed workspaces, keep upstream context compaction upstream-owned and use procedural memory only as a background learning layer.
946
982
 
947
983
  ### `config/catalogs/backends.yaml`
package/README.zh.md CHANGED
@@ -464,9 +464,9 @@ import { AgentHarnessRuntime, createAgentHarness } from "@botbotgo/agent-harness
464
464
  const runtime: AgentHarnessRuntime = await createAgentHarness("/absolute/path/to/workspace");
465
465
  ```
466
466
 
467
- `createAgentHarness(...)` 会加载工作区、解析 `resources/`、在 `runtimeRoot` 下初始化持久化,并启动运行时维护任务。
467
+ `createAgentHarness(...)` 会加载工作区、解析工作区 source、在 `runtimeRoot` 下初始化持久化,并启动运行时维护任务。
468
468
 
469
- `runtime.spec.resources` 可以挂载多个额外资源包。每个条目既可以指向“包根目录下带有 `resources/` 的目录”,也可以直接指向“自身带有 `package.json` 的资源目录”。
469
+ `runtime.spec.sources` 现在是本地工具、npm 工具包与 skill package 的主公开发现入口。
470
470
 
471
471
  ```yaml
472
472
  apiVersion: agent-harness/v1alpha1
@@ -474,11 +474,29 @@ kind: Runtime
474
474
  metadata:
475
475
  name: default
476
476
  spec:
477
- resources:
478
- - ../shared-tools
479
- - file:../shared-skills
477
+ sources:
478
+ tools:
479
+ - file://./resources/tools
480
+ - file://../shared-tools
481
+ - npm://@acme/agent-tools
482
+ skills:
483
+ - file://./resources/skills
484
+ - https://example.com/skills/review/SKILL.md
480
485
  ```
481
486
 
487
+ tool source 规则:
488
+
489
+ - `file://...` 只扫描显式配置的目录
490
+ - `npm://...` 只解析单个包;本地缺失时会自动安装,再从包入口发现导出的 `tool({...})`
491
+ - tool 发现永远不会遍历 `node_modules/**`
492
+
493
+ skill source 规则:
494
+
495
+ - `file://...` 可以指向 skill 集合目录、单个 skill root,或直接指向 `SKILL.md`
496
+ - `http://...` 与 `https://...` 当前只支持一个远程 `SKILL.md`
497
+
498
+ `runtime.spec.resources` 仍保留为兼容旧 attached resource package 的路径。
499
+
482
500
  `createAgentHarness(..., { load })` 支持工作区加载控制。
483
501
 
484
502
  合并顺序是确定性的:
@@ -710,8 +728,10 @@ await stop(runtime);
710
728
 
711
729
  - `config/**` 下的所有 YAML 文档都会被递归发现;文件名与子目录只用于组织,不参与语义
712
730
  - YAML 对象语义由 `kind`、`metadata.name` 或 `id` 以及对象内容决定,而不是由文件路径决定
713
- - 本地工具会从 `resources/tools/**/*.js|*.mjs|*.cjs` 中自动发现,前提是模块导出 `tool({...})`
714
- - skills 会从 `resources/skills/**/SKILL.md` 自动发现
731
+ - `Runtime.spec.sources.tools` 默认值是 `file://./resources/tools`
732
+ - `Runtime.spec.sources.skills` 默认值是 `file://./resources/skills`
733
+ - file-based 工具会从每个声明的 tool source 自动发现,前提是模块导出 `tool({...})`
734
+ - file-based skills 会从每个声明的 skill source 自动发现
715
735
  - 一个最小工作区只放 `config/models.yaml` 也可以启动;仓库默认值会补上 `Runtime`、默认 `orchestra` host,以及默认开启的 runtime-managed durable memory
716
736
  - 如果不显式覆盖 runtime 放置位置,harness 生成的数据默认写到 `./.botbotgo/`
717
737
 
@@ -740,7 +760,9 @@ await stop(runtime);
740
760
 
741
761
  配置大致分这几层(由下至上叠加):
742
762
 
743
- - workspace 启动时扫描本地与附加的 `resources` 包,建立统一 registry
763
+ - 先由 `Runtime.spec.sources` 声明工作区实际拥有的 tool / skill 来源
764
+ - workspace 启动时只扫描这些显式声明的 source,并建立统一 registry
765
+ - workspace 启动时扫描本地与附加的 `resources` 包,建立统一 registry;这条兼容路径仍可继续工作
744
766
  - 各 agent 再按名称白名单选用 tools 与 skills
745
767
  - `config/runtime/workspace.yaml` 承载运行时策略
746
768
  - `config/catalogs/*.yaml` 承载可复用对象目录
@@ -776,6 +798,8 @@ await stop(runtime);
776
798
 
777
799
  - `runtimeRoot`
778
800
  - `concurrency.maxConcurrentRequests`
801
+ - `sources.tools`
802
+ - `sources.skills`
779
803
  - `routing.defaultAgentId`
780
804
  - `routing.rules`
781
805
  - `routing.systemPrompt`
@@ -793,7 +817,17 @@ await stop(runtime);
793
817
  - `maintenance.checkpoints` 清理后端用于 resume/recovery 的 checkpoint 状态
794
818
  - `maintenance.records` 清理 harness 自己保存在 `runtime.sqlite` 中、已结束的 session/request 记录
795
819
 
796
- `toolModuleDiscovery.scope` 用来控制本地 `resources/tools/` 这类 tool module 目录的发现范围:
820
+ `sources.tools` 用来声明哪些 tool root package 参与工作区发现:
821
+
822
+ - `file://...` 用于目录扫描
823
+ - `npm://...` 用于包入口发现;本地缺失时自动安装
824
+
825
+ `sources.skills` 用来声明哪些 skill 目录或 skill 文档参与工作区发现:
826
+
827
+ - `file://...` 用于本地目录、skill root 或直接 `SKILL.md`
828
+ - `http://...` / `https://...` 用于单个远程 `SKILL.md`
829
+
830
+ `toolModuleDiscovery.scope` 用来控制本地 `resources/tools/` 风格 file source 的发现范围:
797
831
 
798
832
  - `recursive` 是默认值,会继续扫描嵌套目录
799
833
  - `top-level` 只发现每个工具根目录下一层文件,同时保留 YAML catalog 的递归发现
@@ -910,6 +944,8 @@ spec:
910
944
  - `KnowledgeRuntime`:hot path + 后台形成 + 长期养护
911
945
  - `ProceduralMemoryRuntime`:后台形成 + 定时或空闲整理
912
946
 
947
+ 在当前已发布的 runtime 里,像“请记住我最近搬到了美国”这种显式 durable fact 仍然走 `KnowledgeRuntime`,并写入 `knowledge/knowledge.sqlite`。后台 procedural learning 会在同一个 data root 下单独写自己的 store 与 state 文件,例如 `knowledge/procedural-memory.sqlite` 和 `knowledge/procedural-memory-state.json`。
948
+
913
949
  对于 `backend: deepagent` 的工作区,应继续把 context compaction 留给上游 DeepAgents,只把 procedural memory 当作后台学习层使用。
914
950
 
915
951
  ### `config/catalogs/backends.yaml`
@@ -29,6 +29,22 @@ spec:
29
29
  # agent-harness feature: stable runtime profile identifier for this data folder.
30
30
  profile: default
31
31
 
32
+ # agent-harness feature: explicit tool and skill discovery sources.
33
+ # The default local workspace contract stays:
34
+ # - tools from ./resources/tools
35
+ # - skills from ./resources/skills
36
+ #
37
+ # Supported source forms today:
38
+ # - tools: file://<folder>, npm://<package-or-spec>
39
+ # - skills: file://<folder-or-SKILL.md>, http(s)://.../SKILL.md
40
+ #
41
+ # Discovery never traverses node_modules directories.
42
+ sources:
43
+ tools:
44
+ - file://./resources/tools
45
+ skills:
46
+ - file://./resources/skills
47
+
32
48
  # agent-harness feature: runtime-level task queue and maximum number of concurrent requests.
33
49
  # Additional requests wait in the harness queue until a slot becomes available.
34
50
  concurrency:
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.305";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.307";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.305";
1
+ export const AGENT_HARNESS_VERSION = "0.0.307";
@@ -1,2 +1,3 @@
1
1
  export { readProceduralMemoryRuntimeConfig } from "./config.js";
2
+ export { createBackgroundProceduralCandidates, createProceduralMemoryManager, ProceduralMemoryFormationSync, } from "./manager.js";
2
3
  export type { ProceduralMemoryBackgroundConfig, ProceduralMemoryFormationConfig, ProceduralMemoryMaintenanceConfig, ProceduralMemoryMaintenanceIdleConfig, ProceduralMemoryMaintenanceScheduleConfig, ProceduralMemoryProviderConfig, ProceduralMemoryRetrievalConfig, ProceduralMemoryRuntimeConfig, } from "./config.js";
@@ -1 +1,2 @@
1
1
  export { readProceduralMemoryRuntimeConfig } from "./config.js";
2
+ export { createBackgroundProceduralCandidates, createProceduralMemoryManager, ProceduralMemoryFormationSync, } from "./manager.js";
@@ -0,0 +1,59 @@
1
+ import type { CompiledAgentBinding, HarnessEvent, HarnessEventProjection, InternalApprovalRecord, MemoryCandidate, MemoryRecord, SessionSummary, TranscriptMessage, WorkspaceBundle } from "../contracts/types.js";
2
+ import type { RuntimePersistence } from "../persistence/types.js";
3
+ import { type StoreLike } from "../runtime/harness/system/store.js";
4
+ import type { ProceduralMemoryRuntimeConfig } from "./config.js";
5
+ type ProceduralMemoryWriter = (input: {
6
+ candidates: MemoryCandidate[];
7
+ sessionId: string;
8
+ requestId: string;
9
+ agentId: string;
10
+ userId?: string;
11
+ projectId?: string;
12
+ recordedAt: string;
13
+ }) => Promise<void>;
14
+ type ProceduralMemoryFormationOptions = {
15
+ userId?: string;
16
+ projectId?: string;
17
+ };
18
+ type ProceduralMemoryManagerLike = {
19
+ transform(input: {
20
+ candidates: MemoryCandidate[];
21
+ binding: CompiledAgentBinding;
22
+ sessionId: string;
23
+ requestId: string;
24
+ recordedAt: string;
25
+ existingRecords: MemoryRecord[];
26
+ }): Promise<MemoryCandidate[]>;
27
+ };
28
+ export declare function createBackgroundProceduralCandidates(input: {
29
+ session: SessionSummary;
30
+ requestId: string;
31
+ agentId: string;
32
+ trigger: "approval.resolved" | "request.completed";
33
+ recordedAt: string;
34
+ messages: TranscriptMessage[];
35
+ approvals: InternalApprovalRecord[];
36
+ focus: string[];
37
+ }): MemoryCandidate[];
38
+ export declare function createProceduralMemoryManager(input: {
39
+ workspace: WorkspaceBundle;
40
+ binding: CompiledAgentBinding;
41
+ config: ProceduralMemoryRuntimeConfig;
42
+ modelResolver?: (modelId: string) => unknown;
43
+ }): ProceduralMemoryManagerLike;
44
+ export declare class ProceduralMemoryFormationSync implements HarnessEventProjection {
45
+ private readonly persistence;
46
+ private readonly config;
47
+ private readonly writer;
48
+ private readonly stateStore;
49
+ private readonly options;
50
+ private readonly pending;
51
+ private syncChain;
52
+ readonly name = "procedural-memory-formation-sync";
53
+ constructor(persistence: RuntimePersistence, config: ProceduralMemoryRuntimeConfig, writer: ProceduralMemoryWriter, runtimeRoot: string, stateStore?: StoreLike, options?: ProceduralMemoryFormationOptions);
54
+ shouldHandle(event: HarnessEvent): boolean;
55
+ handleEvent(event: HarnessEvent): Promise<void>;
56
+ private reflectRun;
57
+ close(): Promise<void>;
58
+ }
59
+ export {};
@@ -0,0 +1,345 @@
1
+ import path from "node:path";
2
+ import { extractMessageText } from "../utils/message-content.js";
3
+ import { createResolvedModel } from "../runtime/adapter/model/model-providers.js";
4
+ import { FileBackedStore } from "../runtime/harness/system/store.js";
5
+ import { compileModel } from "../workspace/resource-compilers.js";
6
+ import { resolveRefId } from "../workspace/support/workspace-ref-utils.js";
7
+ import { renderProceduralMemoryManagerPrompt } from "../runtime/support/runtime-prompts.js";
8
+ const FORMATION_EVENT_TYPES = new Set([
9
+ "request.state.changed",
10
+ "approval.resolved",
11
+ ]);
12
+ function excerpt(message) {
13
+ if (!message?.content) {
14
+ return undefined;
15
+ }
16
+ const normalized = extractMessageText(message.content).replace(/\s+/g, " ").trim();
17
+ if (!normalized) {
18
+ return undefined;
19
+ }
20
+ return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
21
+ }
22
+ function summarizeApprovals(approvals) {
23
+ if (approvals.length === 0) {
24
+ return "No approvals were recorded in this run.";
25
+ }
26
+ return approvals
27
+ .map((approval) => `${approval.toolName} (${approval.status})`)
28
+ .join(", ");
29
+ }
30
+ function fingerprintMessages(messages, focus) {
31
+ const serialized = messages
32
+ .map((message) => `${message.role}\n${message.createdAt}\n${extractMessageText(message.content)}`)
33
+ .join("\n---\n");
34
+ return `${focus.join(",")}::${serialized}`;
35
+ }
36
+ function selectRelatedContextRecords(candidate, existingRecords, maxRecords) {
37
+ const candidateText = `${candidate.summary ?? ""}\n${candidate.content}`.toLowerCase().trim();
38
+ if (!candidateText) {
39
+ return existingRecords.slice(0, maxRecords);
40
+ }
41
+ return existingRecords
42
+ .filter((record) => record.status === "active")
43
+ .map((record) => {
44
+ let score = 0;
45
+ const recordText = `${record.summary}\n${record.content}`.toLowerCase();
46
+ if (candidate.scope && record.scope === candidate.scope) {
47
+ score += 2;
48
+ }
49
+ if (recordText.includes(candidateText) || candidateText.includes(recordText)) {
50
+ score += 4;
51
+ }
52
+ const candidateTokens = new Set(candidateText.split(/[^a-z0-9_\u4e00-\u9fff]+/iu).filter((token) => token.length > 1));
53
+ const recordTokens = new Set(recordText.split(/[^a-z0-9_\u4e00-\u9fff]+/iu).filter((token) => token.length > 1));
54
+ let shared = 0;
55
+ for (const token of candidateTokens) {
56
+ if (recordTokens.has(token)) {
57
+ shared += 1;
58
+ }
59
+ }
60
+ score += shared;
61
+ return { record, score };
62
+ })
63
+ .filter((item) => item.score > 0)
64
+ .sort((left, right) => right.score - left.score || right.record.lastConfirmedAt.localeCompare(left.record.lastConfirmedAt))
65
+ .slice(0, maxRecords)
66
+ .map((item) => item.record);
67
+ }
68
+ function extractText(value) {
69
+ if (typeof value === "string") {
70
+ return value;
71
+ }
72
+ if (typeof value !== "object" || value === null) {
73
+ return undefined;
74
+ }
75
+ if ("content" in value && typeof value.content === "string") {
76
+ return String(value.content);
77
+ }
78
+ const kwargs = "kwargs" in value && typeof value.kwargs === "object" && value.kwargs !== null
79
+ ? value.kwargs
80
+ : undefined;
81
+ if (typeof kwargs?.content === "string") {
82
+ return kwargs.content;
83
+ }
84
+ return undefined;
85
+ }
86
+ function tryParseJsonObject(text) {
87
+ try {
88
+ const parsed = JSON.parse(text);
89
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
90
+ }
91
+ catch {
92
+ const match = text.match(/\{[\s\S]*\}/);
93
+ if (!match) {
94
+ return null;
95
+ }
96
+ try {
97
+ const parsed = JSON.parse(match[0]);
98
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ }
105
+ function asString(value) {
106
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
107
+ }
108
+ function asNumber(value) {
109
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
110
+ }
111
+ function asStringArray(value) {
112
+ if (!Array.isArray(value)) {
113
+ return undefined;
114
+ }
115
+ const items = value
116
+ .filter((item) => typeof item === "string")
117
+ .map((item) => item.trim())
118
+ .filter((item) => item.length > 0);
119
+ return items.length > 0 ? Array.from(new Set(items)) : undefined;
120
+ }
121
+ function normalizeScope(value) {
122
+ return value === "session" || value === "agent" || value === "workspace" || value === "user" || value === "project"
123
+ ? value
124
+ : undefined;
125
+ }
126
+ function normalizeCandidateOutputs(parsed, candidate) {
127
+ const rawOutputs = Array.isArray(parsed.mutations)
128
+ ? parsed.mutations.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
129
+ : [parsed];
130
+ return rawOutputs.map((output) => ({
131
+ ...candidate,
132
+ kind: "procedural",
133
+ scope: normalizeScope(output.scope) ?? candidate.scope ?? "workspace",
134
+ summary: asString(output.summary) ?? candidate.summary,
135
+ content: asString(output.content) ?? candidate.content,
136
+ confidence: asNumber(output.confidence) ?? candidate.confidence ?? 0.72,
137
+ tags: asStringArray(output.tags) ?? candidate.tags,
138
+ })).filter((item) => typeof item.content === "string" && item.content.trim().length > 0);
139
+ }
140
+ export function createBackgroundProceduralCandidates(input) {
141
+ const latestUser = excerpt(input.messages.filter((message) => message.role === "user").at(-1));
142
+ const latestAssistant = excerpt(input.messages.filter((message) => message.role === "assistant").at(-1));
143
+ const transcriptPreview = input.messages
144
+ .slice(-6)
145
+ .map((message) => `${message.role}: ${excerpt(message) ?? "(empty)"}`)
146
+ .join("\n");
147
+ const approvals = summarizeApprovals(input.approvals);
148
+ const sourceRef = `runtime://sessions/${input.session.sessionId}/requests/${input.requestId}/procedural-reflection`;
149
+ return [{
150
+ kind: "procedural",
151
+ scope: "workspace",
152
+ sourceType: "runtime-transcript",
153
+ sourceRef,
154
+ summary: latestUser ?? latestAssistant ?? `Procedural reflection for ${input.requestId}`,
155
+ content: [
156
+ "Completed run transcript evidence for procedural memory extraction.",
157
+ `Trigger: ${input.trigger}`,
158
+ `Focus: ${input.focus.join(", ")}`,
159
+ "",
160
+ "Latest user message:",
161
+ latestUser ?? "(none)",
162
+ "",
163
+ "Latest assistant response:",
164
+ latestAssistant ?? "(none)",
165
+ "",
166
+ "Recent transcript excerpt:",
167
+ transcriptPreview || "(none)",
168
+ "",
169
+ "Approval snapshot:",
170
+ approvals,
171
+ ].join("\n"),
172
+ confidence: 0.64,
173
+ observedAt: input.recordedAt,
174
+ tags: ["procedural-background-extraction", input.trigger, ...input.focus],
175
+ }];
176
+ }
177
+ async function runProceduralMemoryManager(input) {
178
+ let resolvedModel;
179
+ try {
180
+ resolvedModel = await createResolvedModel(input.model, input.modelResolver);
181
+ }
182
+ catch {
183
+ return input.candidates;
184
+ }
185
+ const invoker = resolvedModel;
186
+ if (typeof invoker.invoke !== "function") {
187
+ return input.candidates;
188
+ }
189
+ const transformed = [];
190
+ for (const candidate of input.candidates) {
191
+ const prompt = renderProceduralMemoryManagerPrompt({
192
+ candidate,
193
+ sessionId: input.sessionId,
194
+ requestId: input.requestId,
195
+ focus: input.focus,
196
+ existingRecords: selectRelatedContextRecords(candidate, input.existingRecords, input.maxContextRecords ?? input.existingRecords.length),
197
+ });
198
+ let response;
199
+ try {
200
+ response = await invoker.invoke(prompt, {});
201
+ }
202
+ catch {
203
+ continue;
204
+ }
205
+ const parsed = tryParseJsonObject(extractText(response) ?? "");
206
+ if (!parsed || parsed.store === false) {
207
+ continue;
208
+ }
209
+ transformed.push(...normalizeCandidateOutputs(parsed, candidate));
210
+ }
211
+ return transformed;
212
+ }
213
+ export function createProceduralMemoryManager(input) {
214
+ return {
215
+ async transform({ candidates, binding, sessionId, requestId, recordedAt, existingRecords }) {
216
+ if (input.config.enabled !== true || candidates.length === 0) {
217
+ return candidates;
218
+ }
219
+ if (!binding.langchainAgentParams?.model) {
220
+ return candidates;
221
+ }
222
+ const providerModelRef = asString(input.config.provider?.options?.modelRef);
223
+ const primaryModel = (() => {
224
+ if (!providerModelRef) {
225
+ return binding.langchainAgentParams.model;
226
+ }
227
+ const configured = input.workspace.models.get(resolveRefId(providerModelRef));
228
+ return configured ? compileModel(configured) : binding.langchainAgentParams.model;
229
+ })();
230
+ return runProceduralMemoryManager({
231
+ workspace: input.workspace,
232
+ binding,
233
+ model: primaryModel,
234
+ candidates,
235
+ sessionId,
236
+ requestId,
237
+ recordedAt,
238
+ existingRecords: existingRecords.filter((record) => record.status === "active"),
239
+ focus: input.config.formation?.background?.scopeHints ?? ["workflow_patterns", "debugging_lessons", "reusable_procedures"],
240
+ maxContextRecords: 8,
241
+ modelResolver: input.modelResolver,
242
+ });
243
+ },
244
+ };
245
+ }
246
+ export class ProceduralMemoryFormationSync {
247
+ persistence;
248
+ config;
249
+ writer;
250
+ stateStore;
251
+ options;
252
+ pending = new Set();
253
+ syncChain = Promise.resolve();
254
+ name = "procedural-memory-formation-sync";
255
+ constructor(persistence, config, writer, runtimeRoot, stateStore = new FileBackedStore(path.join(runtimeRoot, config.formation?.background?.stateStorePath ?? "knowledge/procedural-memory-state.json")), options = {}) {
256
+ this.persistence = persistence;
257
+ this.config = config;
258
+ this.writer = writer;
259
+ this.stateStore = stateStore;
260
+ this.options = options;
261
+ }
262
+ shouldHandle(event) {
263
+ const background = this.config.formation?.background;
264
+ if (!background?.enabled || !FORMATION_EVENT_TYPES.has(event.eventType)) {
265
+ return false;
266
+ }
267
+ if (event.eventType === "approval.resolved") {
268
+ return background.writeOnApprovalResolution;
269
+ }
270
+ return background.writeOnRequestCompletion && event.payload.state === "completed";
271
+ }
272
+ async handleEvent(event) {
273
+ if (!this.shouldHandle(event)) {
274
+ return;
275
+ }
276
+ const trigger = event.eventType === "approval.resolved" ? "approval.resolved" : "request.completed";
277
+ const task = this.syncChain
278
+ .then(() => this.reflectRun(event.sessionId, event.requestId, trigger, event.timestamp))
279
+ .catch(() => {
280
+ // Fail open: procedural reflection should not block runtime progress.
281
+ });
282
+ this.syncChain = task
283
+ .catch(() => {
284
+ // Fail open: procedural reflection should not block runtime progress.
285
+ })
286
+ .finally(() => {
287
+ this.pending.delete(task);
288
+ });
289
+ this.pending.add(task);
290
+ }
291
+ async reflectRun(sessionId, requestId, trigger, recordedAt) {
292
+ const background = this.config.formation?.background;
293
+ if (!background) {
294
+ return;
295
+ }
296
+ const [session, run, allMessages, approvals] = await Promise.all([
297
+ this.persistence.getSession(sessionId),
298
+ this.persistence.getRequest(requestId),
299
+ this.persistence.listSessionMessages(sessionId, background.maxMessagesPerRequest),
300
+ this.persistence.getRequestApprovals(sessionId, requestId),
301
+ ]);
302
+ if (!session || !run) {
303
+ return;
304
+ }
305
+ const messages = allMessages.filter((message) => message.requestId === requestId);
306
+ if (messages.length === 0) {
307
+ return;
308
+ }
309
+ const fingerprint = fingerprintMessages(messages, background.scopeHints);
310
+ const namespace = ["memories", "formation", "sessions", sessionId, "requests"];
311
+ const cursor = await this.stateStore.get(namespace, requestId);
312
+ const existing = cursor?.value;
313
+ if (existing?.fingerprint === fingerprint && existing.trigger === trigger) {
314
+ return;
315
+ }
316
+ const candidates = createBackgroundProceduralCandidates({
317
+ session,
318
+ requestId,
319
+ agentId: run.agentId ?? session.agentId,
320
+ trigger,
321
+ recordedAt,
322
+ messages,
323
+ approvals,
324
+ focus: background.scopeHints,
325
+ });
326
+ await this.writer({
327
+ candidates,
328
+ sessionId,
329
+ requestId,
330
+ agentId: run.agentId ?? session.agentId,
331
+ userId: this.options.userId,
332
+ projectId: this.options.projectId,
333
+ recordedAt,
334
+ });
335
+ await this.stateStore.put(namespace, requestId, {
336
+ fingerprint,
337
+ candidateCount: candidates.length,
338
+ syncedAt: new Date().toISOString(),
339
+ trigger,
340
+ });
341
+ }
342
+ async close() {
343
+ await Promise.allSettled(Array.from(this.pending));
344
+ }
345
+ }