@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 +45 -9
- package/README.zh.md +45 -9
- package/dist/config/runtime/workspace.yaml +16 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/procedural/index.d.ts +1 -0
- package/dist/procedural/index.js +1 -0
- package/dist/procedural/manager.d.ts +59 -0
- package/dist/procedural/manager.js +345 -0
- package/dist/resource/isolation.js +6 -1
- package/dist/resources/prompts/runtime/procedural-memory-manager.md +40 -0
- package/dist/runtime/adapter/model/invocation-request.d.ts +1 -0
- package/dist/runtime/adapter/model/invocation-request.js +4 -0
- package/dist/runtime/harness.d.ts +6 -0
- package/dist/runtime/harness.js +144 -14
- package/dist/runtime/support/runtime-prompts.d.ts +7 -0
- package/dist/runtime/support/runtime-prompts.js +14 -0
- package/dist/workspace/compile.js +135 -5
- package/dist/workspace/object-loader.js +27 -17
- package/dist/workspace/resource-compilers.js +3 -3
- package/dist/workspace/support/source-protocols.d.ts +19 -0
- package/dist/workspace/support/source-protocols.js +192 -0
- package/dist/workspace/support/workspace-ref-utils.d.ts +4 -0
- package/dist/workspace/support/workspace-ref-utils.js +4 -0
- package/package.json +1 -1
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
|
|
472
|
+
`createAgentHarness(...)` loads the workspace, resolves workspace sources, initializes persistence under `runtimeRoot`, and starts runtime maintenance.
|
|
473
473
|
|
|
474
|
-
`runtime.spec.
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
-
|
|
743
|
-
- skills
|
|
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
|
|
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
|
-
`
|
|
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(...)`
|
|
467
|
+
`createAgentHarness(...)` 会加载工作区、解析工作区 source、在 `runtimeRoot` 下初始化持久化,并启动运行时维护任务。
|
|
468
468
|
|
|
469
|
-
`runtime.spec.
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
-
|
|
714
|
-
- skills
|
|
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
|
-
-
|
|
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
|
-
`
|
|
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.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.307";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
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";
|
package/dist/procedural/index.js
CHANGED
|
@@ -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
|
+
}
|