@kuralle-agents/core 0.4.1 → 0.6.0

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 (70) hide show
  1. package/README.md +12 -3
  2. package/dist/ai-sdk/uiMessageStream.d.ts +51 -0
  3. package/dist/ai-sdk/uiMessageStream.js +164 -0
  4. package/dist/events/TurnHandle.js +7 -0
  5. package/dist/flow/nodeBuilders.d.ts +1 -1
  6. package/dist/flow/nodeBuilders.js +5 -3
  7. package/dist/index.d.ts +14 -4
  8. package/dist/index.js +8 -2
  9. package/dist/memory/blocks/FilePersistentMemoryStore.d.ts +1 -1
  10. package/dist/memory/blocks/FilePersistentMemoryStore.js +9 -0
  11. package/dist/memory/blocks/InMemoryPersistentMemoryStore.d.ts +8 -0
  12. package/dist/memory/blocks/InMemoryPersistentMemoryStore.js +25 -0
  13. package/dist/memory/blocks/RoutedPersistentMemoryStore.d.ts +18 -0
  14. package/dist/memory/blocks/RoutedPersistentMemoryStore.js +35 -0
  15. package/dist/memory/blocks/TieredPersistentMemoryStore.d.ts +10 -0
  16. package/dist/memory/blocks/TieredPersistentMemoryStore.js +30 -0
  17. package/dist/memory/blocks/memoryBlockTool.d.ts +5 -5
  18. package/dist/memory/blocks/testing.d.ts +11 -0
  19. package/dist/memory/blocks/testing.js +59 -0
  20. package/dist/memory/index.d.ts +4 -0
  21. package/dist/memory/index.js +3 -0
  22. package/dist/runtime/Runtime.d.ts +3 -0
  23. package/dist/runtime/Runtime.js +47 -5
  24. package/dist/runtime/agentReply.js +2 -1
  25. package/dist/runtime/channels/TextDriver.js +3 -2
  26. package/dist/runtime/channels/VoiceDriver.js +3 -3
  27. package/dist/runtime/channels/extractionTurn.js +1 -1
  28. package/dist/runtime/ctx.d.ts +2 -0
  29. package/dist/runtime/ctx.js +1 -0
  30. package/dist/runtime/grounding/defaultStoreRegistry.d.ts +3 -0
  31. package/dist/runtime/grounding/defaultStoreRegistry.js +10 -0
  32. package/dist/runtime/grounding/index.d.ts +1 -0
  33. package/dist/runtime/grounding/index.js +1 -0
  34. package/dist/runtime/grounding/workingMemory.d.ts +19 -0
  35. package/dist/runtime/grounding/workingMemory.js +80 -0
  36. package/dist/runtime/resolveAgentWorkspace.d.ts +10 -0
  37. package/dist/runtime/resolveAgentWorkspace.js +9 -0
  38. package/dist/skills/SkillsCapability.d.ts +10 -0
  39. package/dist/skills/SkillsCapability.js +52 -0
  40. package/dist/skills/collectSkills.d.ts +17 -0
  41. package/dist/skills/collectSkills.js +56 -0
  42. package/dist/skills/index.d.ts +5 -0
  43. package/dist/skills/index.js +4 -0
  44. package/dist/skills/inlineSkillStore.d.ts +9 -0
  45. package/dist/skills/inlineSkillStore.js +37 -0
  46. package/dist/skills/wireAgentSkills.d.ts +10 -0
  47. package/dist/skills/wireAgentSkills.js +25 -0
  48. package/dist/testing/mocks.js +7 -0
  49. package/dist/tools/effect/defineTool.js +1 -1
  50. package/dist/tools/effect/index.d.ts +1 -0
  51. package/dist/tools/effect/index.js +1 -0
  52. package/dist/tools/effect/wrapAiSdkTool.d.ts +3 -0
  53. package/dist/tools/effect/wrapAiSdkTool.js +12 -0
  54. package/dist/tools/fs/createFsTool.d.ts +30 -0
  55. package/dist/tools/fs/createFsTool.js +200 -0
  56. package/dist/types/agentConfig.d.ts +16 -3
  57. package/dist/types/filesystem.d.ts +85 -0
  58. package/dist/types/filesystem.js +6 -0
  59. package/dist/types/grounding.d.ts +12 -0
  60. package/dist/types/index.d.ts +2 -0
  61. package/dist/types/index.js +2 -0
  62. package/dist/types/run-context.d.ts +11 -2
  63. package/dist/types/skills.d.ts +19 -0
  64. package/dist/types/skills.js +1 -0
  65. package/dist/types/stream.d.ts +3 -0
  66. package/guides/AGENTS.md +2 -3
  67. package/guides/FLOWS.md +2 -2
  68. package/guides/RUNTIME.md +18 -1
  69. package/guides/TOOLS.md +69 -2
  70. package/package.json +12 -3
package/README.md CHANGED
@@ -41,7 +41,7 @@ const agent = defineAgent({
41
41
  instructions: 'You are a helpful support agent.',
42
42
  model: openai('gpt-4o-mini'),
43
43
  tools: buildToolSet({ echo }), // model-visible
44
- effectTools: { echo }, // durable executor
44
+ tools: { echo }, // durable executor
45
45
  });
46
46
 
47
47
  const runtime = createRuntime({ agents: [agent], defaultAgentId: 'support' });
@@ -130,9 +130,18 @@ const runtime = createRuntime({
130
130
  });
131
131
  ```
132
132
 
133
- ## HTTP streaming
133
+ ## HTTP streaming (web)
134
134
 
135
- `TurnHandle` exposes `handle.toResponseStream('sse')` for HTTP transports or use `@kuralle-agents/hono-server` for a ready-made Hono router.
135
+ For React/web consumers, return a native AI SDK `UIMessageStream` `useChat` works with no bridge:
136
+
137
+ ```ts
138
+ const handle = runtime.run({ input: 'Hello', sessionId: 'demo' });
139
+ return handle.toUIMessageStreamResponse({ sessionId: 'demo' });
140
+ ```
141
+
142
+ Kuralle orchestration events (flow telemetry, safety blocks, interactive choices) arrive as typed `data-kuralle-*` parts. Import `KuralleUIMessage` and `KuralleDataParts` for compile-time-safe `message.parts` and `useChat({ onData })` handlers.
143
+
144
+ For non-UI consumers (curl, custom transports), use `handle.toResponseStream('sse')` to emit raw `HarnessStreamPart` JSON-SSE. Or use `@kuralle-agents/hono-server` — `POST /api/chat/sse` defaults to native `UIMessageStream`; append `?format=raw` for the legacy wire.
136
145
 
137
146
  ## Related
138
147
 
@@ -0,0 +1,51 @@
1
+ import { type UIMessage } from 'ai';
2
+ import type { ConversationOutcome } from '../outcomes/types.js';
3
+ import type { ChoiceOption } from '../types/selection.js';
4
+ import type { HarnessStreamPart } from '../types/stream.js';
5
+ export type KuralleMetadata = {
6
+ sessionId?: string;
7
+ };
8
+ export type KuralleDataParts = {
9
+ 'kuralle-node': {
10
+ event: 'enter' | 'exit';
11
+ node: string;
12
+ };
13
+ 'kuralle-flow': {
14
+ event: 'enter' | 'transition' | 'end';
15
+ flow?: string;
16
+ from?: string;
17
+ to?: string;
18
+ reason?: string;
19
+ };
20
+ 'kuralle-handoff': {
21
+ targetAgent: string;
22
+ reason?: string;
23
+ };
24
+ 'kuralle-interactive': {
25
+ nodeId: string;
26
+ prompt: string;
27
+ options: ChoiceOption[];
28
+ };
29
+ 'kuralle-safety': {
30
+ kind: 'safety-blocked' | 'pipeline-validation-block';
31
+ moderator?: string;
32
+ rationale: string;
33
+ userFacingMessage?: string;
34
+ };
35
+ 'kuralle-outcome': {
36
+ outcome: ConversationOutcome;
37
+ };
38
+ 'kuralle-control': {
39
+ event: 'interrupted' | 'paused';
40
+ reason?: string;
41
+ waitingFor?: string;
42
+ };
43
+ 'kuralle-custom': {
44
+ name: string;
45
+ data: unknown;
46
+ };
47
+ };
48
+ export type KuralleUIMessage = UIMessage<KuralleMetadata, KuralleDataParts>;
49
+ export declare function harnessToUIMessageStream(source: AsyncIterable<HarnessStreamPart>, opts?: {
50
+ sessionId?: string;
51
+ }): ReadableStream;
@@ -0,0 +1,164 @@
1
+ import { createUIMessageStream, generateId, } from 'ai';
2
+ function writeHarnessPart(part, writer) {
3
+ switch (part.type) {
4
+ case 'text-start':
5
+ writer.write({ type: 'text-start', id: part.id });
6
+ break;
7
+ case 'text-delta':
8
+ writer.write({ type: 'text-delta', id: part.id, delta: part.delta });
9
+ break;
10
+ case 'text-end':
11
+ writer.write({ type: 'text-end', id: part.id });
12
+ break;
13
+ case 'text-cancel':
14
+ writer.write({ type: 'text-end', id: part.id });
15
+ break;
16
+ case 'tool-call':
17
+ writer.write({
18
+ type: 'tool-input-available',
19
+ toolCallId: part.toolCallId ?? generateId(),
20
+ toolName: part.toolName,
21
+ input: part.args,
22
+ });
23
+ break;
24
+ case 'tool-result':
25
+ writer.write({
26
+ type: 'tool-output-available',
27
+ toolCallId: part.toolCallId ?? 'unknown',
28
+ output: part.result,
29
+ });
30
+ break;
31
+ case 'node-enter':
32
+ writer.write({
33
+ type: 'data-kuralle-node',
34
+ data: { event: 'enter', node: part.nodeName },
35
+ transient: true,
36
+ });
37
+ break;
38
+ case 'node-exit':
39
+ writer.write({
40
+ type: 'data-kuralle-node',
41
+ data: { event: 'exit', node: part.nodeName },
42
+ transient: true,
43
+ });
44
+ break;
45
+ case 'flow-enter':
46
+ writer.write({
47
+ type: 'data-kuralle-flow',
48
+ data: { event: 'enter', flow: part.flow },
49
+ transient: true,
50
+ });
51
+ break;
52
+ case 'flow-end':
53
+ writer.write({
54
+ type: 'data-kuralle-flow',
55
+ data: { event: 'end', flow: part.flow, reason: part.reason },
56
+ transient: true,
57
+ });
58
+ break;
59
+ case 'flow-transition':
60
+ writer.write({
61
+ type: 'data-kuralle-flow',
62
+ data: { event: 'transition', from: part.from, to: part.to },
63
+ transient: true,
64
+ });
65
+ break;
66
+ case 'handoff':
67
+ writer.write({
68
+ type: 'data-kuralle-handoff',
69
+ id: generateId(),
70
+ data: { targetAgent: part.targetAgent, reason: part.reason },
71
+ });
72
+ break;
73
+ case 'interactive':
74
+ writer.write({
75
+ type: 'data-kuralle-interactive',
76
+ id: part.nodeId,
77
+ data: { nodeId: part.nodeId, prompt: part.prompt, options: part.options },
78
+ });
79
+ break;
80
+ case 'safety-blocked':
81
+ writer.write({
82
+ type: 'data-kuralle-safety',
83
+ id: generateId(),
84
+ data: {
85
+ kind: 'safety-blocked',
86
+ moderator: part.moderator,
87
+ rationale: part.rationale,
88
+ userFacingMessage: part.userFacingMessage,
89
+ },
90
+ });
91
+ break;
92
+ case 'pipeline-validation-block':
93
+ writer.write({
94
+ type: 'data-kuralle-safety',
95
+ id: generateId(),
96
+ data: {
97
+ kind: 'pipeline-validation-block',
98
+ rationale: part.rationale,
99
+ userFacingMessage: part.userFacingMessage,
100
+ },
101
+ });
102
+ break;
103
+ case 'conversation-outcome':
104
+ writer.write({
105
+ type: 'data-kuralle-outcome',
106
+ id: generateId(),
107
+ data: { outcome: part.outcome },
108
+ });
109
+ break;
110
+ case 'interrupted':
111
+ writer.write({
112
+ type: 'data-kuralle-control',
113
+ data: { event: 'interrupted', reason: part.reason },
114
+ transient: true,
115
+ });
116
+ break;
117
+ case 'paused':
118
+ writer.write({
119
+ type: 'data-kuralle-control',
120
+ data: { event: 'paused', waitingFor: part.waitingFor },
121
+ transient: true,
122
+ });
123
+ break;
124
+ case 'custom':
125
+ writer.write({
126
+ type: 'data-kuralle-custom',
127
+ data: { name: part.name, data: part.data },
128
+ transient: true,
129
+ });
130
+ break;
131
+ case 'error':
132
+ throw new Error(part.error);
133
+ case 'done':
134
+ case 'turn-end':
135
+ break;
136
+ }
137
+ }
138
+ export function harnessToUIMessageStream(source, opts) {
139
+ return createUIMessageStream({
140
+ execute: async ({ writer }) => {
141
+ let doneSessionId = opts?.sessionId;
142
+ if (doneSessionId) {
143
+ writer.write({
144
+ type: 'start',
145
+ messageMetadata: { sessionId: doneSessionId },
146
+ });
147
+ }
148
+ for await (const part of source) {
149
+ if (part.type === 'done' && part.sessionId) {
150
+ doneSessionId = doneSessionId ?? part.sessionId;
151
+ }
152
+ else {
153
+ writeHarnessPart(part, writer);
154
+ }
155
+ }
156
+ if (doneSessionId) {
157
+ writer.write({
158
+ type: 'finish',
159
+ messageMetadata: { sessionId: doneSessionId },
160
+ });
161
+ }
162
+ },
163
+ });
164
+ }
@@ -1,3 +1,5 @@
1
+ import { createUIMessageStreamResponse } from 'ai';
2
+ import { harnessToUIMessageStream } from '../ai-sdk/uiMessageStream.js';
1
3
  export function createEventBus() {
2
4
  const events = [];
3
5
  const waiters = [];
@@ -48,6 +50,11 @@ export function createTurnHandle(options) {
48
50
  toResponseStream(format = 'sse') {
49
51
  return createResponseStream(bus.events(), format);
50
52
  },
53
+ toUIMessageStreamResponse(opts) {
54
+ return createUIMessageStreamResponse({
55
+ stream: harnessToUIMessageStream(bus.events(), opts),
56
+ });
57
+ },
51
58
  cancel(reason) {
52
59
  abortController.abort(reason);
53
60
  bus.close();
@@ -8,7 +8,7 @@ export declare function buildNodePrompt(node: ReplyNode, state: FlowState): stri
8
8
  * agent's base instructions (persona / safety / grounding) prefix the node's
9
9
  * own instructions. Node instructions layer ON TOP — they never replace the
10
10
  * base. Base resolves against the current state so dynamic base prompts work. */
11
- export declare function composeSystem(base: Instructions | undefined, nodeSystem: string, state: FlowState): string;
11
+ export declare function composeSystem(base: Instructions | undefined, nodeSystem: string, state: FlowState, skillPrompt?: string, workingMemoryPrompt?: string): string;
12
12
  export declare function resolveReplyNode(node: ReplyNode, state: FlowState, options?: {
13
13
  freeConversation?: boolean;
14
14
  }): ResolvedNode;
@@ -19,9 +19,11 @@ export function buildNodePrompt(node, state) {
19
19
  * agent's base instructions (persona / safety / grounding) prefix the node's
20
20
  * own instructions. Node instructions layer ON TOP — they never replace the
21
21
  * base. Base resolves against the current state so dynamic base prompts work. */
22
- export function composeSystem(base, nodeSystem, state) {
22
+ export function composeSystem(base, nodeSystem, state, skillPrompt, workingMemoryPrompt) {
23
23
  const baseText = base ? resolveInstructions(base, state) : '';
24
- return [baseText, nodeSystem].filter((s) => s && s.trim()).join('\n\n');
24
+ return [baseText, skillPrompt, workingMemoryPrompt, nodeSystem]
25
+ .filter((s) => s && s.trim())
26
+ .join('\n\n');
25
27
  }
26
28
  function buildNodeTools(node, state) {
27
29
  if (!node.tools) {
@@ -39,7 +41,7 @@ export function resolveReplyNode(node, state, options) {
39
41
  prompt: buildNodePrompt(node, state),
40
42
  tools,
41
43
  // Recover the raw executors from the node's `buildToolSet` tools so they run
42
- // in-flow (with run context) — without also needing `agent.effectTools`.
44
+ // in-flow (with run context) — without also needing `agent.tools`.
43
45
  localTools: rawToolsFromSet(tools),
44
46
  ...(options?.freeConversation && { freeConversation: true }),
45
47
  };
package/dist/index.d.ts CHANGED
@@ -47,8 +47,11 @@ export type { MemoryEntry, SearchMemoryRequest, SearchMemoryResponse, MemoryInge
47
47
  export { InMemoryMemoryService } from './memory/index.js';
48
48
  export { preloadMemoryContext } from './memory/index.js';
49
49
  export { extractMemories } from './memory/index.js';
50
- export type { PersistentMemoryStore, PersistentMemoryBlock, PersistentMemoryConfig, MemoryBlockScope, } from './memory/index.js';
51
- export { FilePersistentMemoryStore, scanMemoryWrite, buildMemoryBlockTool, DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './memory/index.js';
50
+ export type { PersistentMemoryStore, PersistentMemoryBlock, PersistentMemoryConfig, MemoryBlockScope, WorkingMemoryBlockSpec, WorkingMemoryConfig, } from './memory/index.js';
51
+ export { FilePersistentMemoryStore, InMemoryPersistentMemoryStore, RoutedPersistentMemoryStore, TieredPersistentMemoryStore, scanMemoryWrite, buildMemoryBlockTool, DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './memory/index.js';
52
+ export type { RoutedPersistentMemoryStoreConfig, MemoryRouteFn, } from './memory/index.js';
53
+ export { wireWorkingMemory, loadWorkingMemoryBlocks, formatWorkingMemorySection, resolveWorkingMemoryStore, } from './runtime/grounding/workingMemory.js';
54
+ export { resolveAgentWorkspace, type ResolvedAgentWorkspace, } from './runtime/resolveAgentWorkspace.js';
52
55
  export { DEFAULT_CONTEXT_BUDGET, VOICE_CONTEXT_BUDGET, computeMessageHistoryBudget, truncateToTokenBudget, formatMemoryWithBudget, estimateTokenCount, ContextBudget, } from './runtime/ContextBudget.js';
53
56
  export type { ContextBudgetConfig } from './runtime/ContextBudget.js';
54
57
  export { TokenAccumulator } from './runtime/TokenAccumulator.js';
@@ -67,16 +70,23 @@ export type { Hooks } from './types/hooks.js';
67
70
  export type { HarnessHooks } from './types/runtime.js';
68
71
  export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, } from './authoring/index.js';
69
72
  export { defineTool } from './types/effectTool.js';
70
- export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
73
+ export { fsErrorCode } from './types/filesystem.js';
74
+ export { createFsTool } from './tools/fs/createFsTool.js';
75
+ export type { CreateFsToolOptions, GrepHit } from './tools/fs/createFsTool.js';
76
+ export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
77
+ export type { WiredAgentSkills, SkillWireAgent } from './skills/index.js';
78
+ export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
71
79
  export type { Tool as EffectTool } from './types/effectTool.js';
72
80
  export type { AgentRoute } from './types/processors.js';
73
- export type { AgentConfig, Instructions } from './types/agentConfig.js';
81
+ export type { AgentConfig, AgentWorkspaceConfig, Instructions } from './types/agentConfig.js';
74
82
  export type { Flow, FlowNode, Transition, CollectNode, DecideNode, ConfirmGate, NodeGrounding, } from './types/flow.js';
75
83
  export { parseConfirmation } from './flow/confirmParse.js';
76
84
  export type { ConfirmVerdict } from './flow/confirmParse.js';
77
85
  export type { Route } from './types/route.js';
78
86
  export type { TurnHandle } from './types/stream.js';
79
87
  export type { HarnessStreamPart } from './types/stream.js';
88
+ export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
89
+ export type { KuralleMetadata, KuralleDataParts, KuralleUIMessage, } from './ai-sdk/uiMessageStream.js';
80
90
  export type { ChoiceOption, ResolvedSelection } from './types/selection.js';
81
91
  export type { RunState, StepRecord } from './runtime/durable/types.js';
82
92
  export type { RunStore } from './runtime/durable/RunStore.js';
package/dist/index.js CHANGED
@@ -29,7 +29,9 @@ export { getTemplate, registerTemplate, listTemplates, getAllTemplates } from '.
29
29
  export { InMemoryMemoryService } from './memory/index.js';
30
30
  export { preloadMemoryContext } from './memory/index.js';
31
31
  export { extractMemories } from './memory/index.js';
32
- export { FilePersistentMemoryStore, scanMemoryWrite, buildMemoryBlockTool, DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './memory/index.js';
32
+ export { FilePersistentMemoryStore, InMemoryPersistentMemoryStore, RoutedPersistentMemoryStore, TieredPersistentMemoryStore, scanMemoryWrite, buildMemoryBlockTool, DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './memory/index.js';
33
+ export { wireWorkingMemory, loadWorkingMemoryBlocks, formatWorkingMemorySection, resolveWorkingMemoryStore, } from './runtime/grounding/workingMemory.js';
34
+ export { resolveAgentWorkspace, } from './runtime/resolveAgentWorkspace.js';
33
35
  export { DEFAULT_CONTEXT_BUDGET, VOICE_CONTEXT_BUDGET, computeMessageHistoryBudget, truncateToTokenBudget, formatMemoryWithBudget, estimateTokenCount, ContextBudget, } from './runtime/ContextBudget.js';
34
36
  export { TokenAccumulator } from './runtime/TokenAccumulator.js';
35
37
  export { handoffFilters, removeToolHistory, keepRecentMessages, removeKeys, composeFilters, } from './runtime/handoffFilters.js';
@@ -38,6 +40,10 @@ export { CapabilityHost, TriageCapability, ExtractionCapability, HandoffCapabili
38
40
  export { filterAuditEntries } from './audit/index.js';
39
41
  export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, } from './authoring/index.js';
40
42
  export { defineTool } from './types/effectTool.js';
41
- export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
43
+ export { fsErrorCode } from './types/filesystem.js';
44
+ export { createFsTool } from './tools/fs/createFsTool.js';
45
+ export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
46
+ export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
42
47
  export { parseConfirmation } from './flow/confirmParse.js';
48
+ export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
43
49
  export { createRuntime, Runtime, } from './runtime/Runtime.js';
@@ -1,4 +1,4 @@
1
- import { type PersistentMemoryStore, type PersistentMemoryBlock, type MemoryBlockScope } from './types.js';
1
+ import type { PersistentMemoryStore, PersistentMemoryBlock, MemoryBlockScope } from './types.js';
2
2
  export interface FilePersistentMemoryStoreOptions {
3
3
  /** Root directory. Defaults to `KURALLE_MEMORY_DIR` or `~/.kuralle/memories`. */
4
4
  rootDir?: string;
@@ -9,6 +9,13 @@
9
9
  *
10
10
  * Defaults to `process.env.KURALLE_MEMORY_DIR ?? <homedir>/.kuralle/memories`.
11
11
  *
12
+ * Relationship to `AgentConfig.workspace` (RFC-02): this store persists long-term
13
+ * memory *blocks* on the local Node filesystem via `node:fs`. It is not a
14
+ * portable `FileSystem` backend and is not wired into the agent workspace tool.
15
+ * Use `@kuralle-agents/fs` (`InMemoryFs`, `KnowledgeFs`, …) for model-visible
16
+ * workspace exploration; keep `FilePersistentMemoryStore` for durable USER/AGENT
17
+ * markdown blocks when running on Node.
18
+ *
12
19
  * Atomicity: writes go to a sibling `.tmp` file then `rename` over the
13
20
  * target. `rename` is atomic on POSIX within a single filesystem;
14
21
  * Windows is best-effort.
@@ -21,6 +28,7 @@
21
28
  import { promises as fs } from 'node:fs';
22
29
  import os from 'node:os';
23
30
  import path from 'node:path';
31
+ import { registerNodeDefaultWorkingMemoryStore } from '../../runtime/grounding/defaultStoreRegistry.js';
24
32
  export class FilePersistentMemoryStore {
25
33
  rootDir;
26
34
  constructor(opts = {}) {
@@ -111,3 +119,4 @@ export class FilePersistentMemoryStore {
111
119
  function isNodeError(err) {
112
120
  return typeof err === 'object' && err !== null && 'code' in err;
113
121
  }
122
+ registerNodeDefaultWorkingMemoryStore(() => new FilePersistentMemoryStore());
@@ -0,0 +1,8 @@
1
+ import type { MemoryBlockScope, PersistentMemoryBlock, PersistentMemoryStore } from './types.js';
2
+ export declare class InMemoryPersistentMemoryStore implements PersistentMemoryStore {
3
+ private readonly blocks;
4
+ loadBlock(scope: MemoryBlockScope, owner: string, key: string): Promise<PersistentMemoryBlock | null>;
5
+ saveBlock(block: PersistentMemoryBlock, owner: string): Promise<void>;
6
+ deleteBlock(scope: MemoryBlockScope, owner: string, key: string): Promise<void>;
7
+ listBlocks(scope: MemoryBlockScope, owner: string): Promise<string[]>;
8
+ }
@@ -0,0 +1,25 @@
1
+ function blockKey(scope, owner, key) {
2
+ return `${scope}:${owner}:${key}`;
3
+ }
4
+ export class InMemoryPersistentMemoryStore {
5
+ blocks = new Map();
6
+ async loadBlock(scope, owner, key) {
7
+ return this.blocks.get(blockKey(scope, owner, key)) ?? null;
8
+ }
9
+ async saveBlock(block, owner) {
10
+ this.blocks.set(blockKey(block.scope, owner, block.key), {
11
+ ...block,
12
+ updatedAt: new Date().toISOString(),
13
+ });
14
+ }
15
+ async deleteBlock(scope, owner, key) {
16
+ this.blocks.delete(blockKey(scope, owner, key));
17
+ }
18
+ async listBlocks(scope, owner) {
19
+ const prefix = `${scope}:${owner}:`;
20
+ return [...this.blocks.keys()]
21
+ .filter((k) => k.startsWith(prefix))
22
+ .map((k) => k.slice(prefix.length))
23
+ .sort();
24
+ }
25
+ }
@@ -0,0 +1,18 @@
1
+ import type { MemoryBlockScope, PersistentMemoryBlock, PersistentMemoryStore } from './types.js';
2
+ export type MemoryRouteFn = (scope: MemoryBlockScope, owner: string) => PersistentMemoryStore;
3
+ export interface RoutedPersistentMemoryStoreConfig {
4
+ routes: Partial<Record<MemoryBlockScope, PersistentMemoryStore>>;
5
+ default?: PersistentMemoryStore;
6
+ route?: MemoryRouteFn;
7
+ }
8
+ export declare class RoutedPersistentMemoryStore implements PersistentMemoryStore {
9
+ private readonly routes;
10
+ private readonly defaultStore?;
11
+ private readonly routeFn?;
12
+ constructor(config: RoutedPersistentMemoryStoreConfig);
13
+ private resolve;
14
+ loadBlock(scope: MemoryBlockScope, owner: string, key: string): Promise<PersistentMemoryBlock | null>;
15
+ saveBlock(block: PersistentMemoryBlock, owner: string): Promise<void>;
16
+ deleteBlock(scope: MemoryBlockScope, owner: string, key: string): Promise<void>;
17
+ listBlocks(scope: MemoryBlockScope, owner: string): Promise<string[]>;
18
+ }
@@ -0,0 +1,35 @@
1
+ export class RoutedPersistentMemoryStore {
2
+ routes;
3
+ defaultStore;
4
+ routeFn;
5
+ constructor(config) {
6
+ this.routes = config.routes;
7
+ this.defaultStore = config.default;
8
+ this.routeFn = config.route;
9
+ }
10
+ resolve(scope, owner) {
11
+ if (this.routeFn) {
12
+ return this.routeFn(scope, owner);
13
+ }
14
+ const byScope = this.routes[scope];
15
+ if (byScope) {
16
+ return byScope;
17
+ }
18
+ if (this.defaultStore) {
19
+ return this.defaultStore;
20
+ }
21
+ throw new Error(`No PersistentMemoryStore route for scope=${scope}`);
22
+ }
23
+ async loadBlock(scope, owner, key) {
24
+ return this.resolve(scope, owner).loadBlock(scope, owner, key);
25
+ }
26
+ async saveBlock(block, owner) {
27
+ await this.resolve(block.scope, owner).saveBlock(block, owner);
28
+ }
29
+ async deleteBlock(scope, owner, key) {
30
+ await this.resolve(scope, owner).deleteBlock(scope, owner, key);
31
+ }
32
+ async listBlocks(scope, owner) {
33
+ return this.resolve(scope, owner).listBlocks(scope, owner);
34
+ }
35
+ }
@@ -0,0 +1,10 @@
1
+ import type { MemoryBlockScope, PersistentMemoryBlock, PersistentMemoryStore } from './types.js';
2
+ export declare class TieredPersistentMemoryStore implements PersistentMemoryStore {
3
+ private readonly cache;
4
+ private readonly durable;
5
+ constructor(cache: PersistentMemoryStore, durable: PersistentMemoryStore);
6
+ loadBlock(scope: MemoryBlockScope, owner: string, key: string): Promise<PersistentMemoryBlock | null>;
7
+ saveBlock(block: PersistentMemoryBlock, owner: string): Promise<void>;
8
+ deleteBlock(scope: MemoryBlockScope, owner: string, key: string): Promise<void>;
9
+ listBlocks(scope: MemoryBlockScope, owner: string): Promise<string[]>;
10
+ }
@@ -0,0 +1,30 @@
1
+ export class TieredPersistentMemoryStore {
2
+ cache;
3
+ durable;
4
+ constructor(cache, durable) {
5
+ this.cache = cache;
6
+ this.durable = durable;
7
+ }
8
+ async loadBlock(scope, owner, key) {
9
+ const cached = await this.cache.loadBlock(scope, owner, key);
10
+ if (cached) {
11
+ return cached;
12
+ }
13
+ const fromDurable = await this.durable.loadBlock(scope, owner, key);
14
+ if (fromDurable) {
15
+ await this.cache.saveBlock(fromDurable, owner);
16
+ }
17
+ return fromDurable;
18
+ }
19
+ async saveBlock(block, owner) {
20
+ await this.durable.saveBlock(block, owner);
21
+ await this.cache.saveBlock(block, owner);
22
+ }
23
+ async deleteBlock(scope, owner, key) {
24
+ await this.durable.deleteBlock(scope, owner, key);
25
+ await this.cache.deleteBlock(scope, owner, key);
26
+ }
27
+ async listBlocks(scope, owner) {
28
+ return this.durable.listBlocks(scope, owner);
29
+ }
30
+ }
@@ -21,7 +21,7 @@ export declare function buildMemoryBlockTool(opts: MemoryBlockToolOptions): impo
21
21
  empty: boolean;
22
22
  updatedAt?: undefined;
23
23
  chars?: undefined;
24
- readonly error?: undefined;
24
+ error?: undefined;
25
25
  message?: undefined;
26
26
  ok?: undefined;
27
27
  removed?: undefined;
@@ -38,7 +38,7 @@ export declare function buildMemoryBlockTool(opts: MemoryBlockToolOptions): impo
38
38
  updatedAt: string | undefined;
39
39
  chars: number;
40
40
  empty?: undefined;
41
- readonly error?: undefined;
41
+ error?: undefined;
42
42
  message?: undefined;
43
43
  ok?: undefined;
44
44
  removed?: undefined;
@@ -75,7 +75,7 @@ export declare function buildMemoryBlockTool(opts: MemoryBlockToolOptions): impo
75
75
  empty?: undefined;
76
76
  updatedAt?: undefined;
77
77
  chars?: undefined;
78
- readonly error?: undefined;
78
+ error?: undefined;
79
79
  message?: undefined;
80
80
  remainingChars?: undefined;
81
81
  pattern?: undefined;
@@ -92,7 +92,7 @@ export declare function buildMemoryBlockTool(opts: MemoryBlockToolOptions): impo
92
92
  empty?: undefined;
93
93
  updatedAt?: undefined;
94
94
  chars?: undefined;
95
- readonly error?: undefined;
95
+ error?: undefined;
96
96
  message?: undefined;
97
97
  note?: undefined;
98
98
  pattern?: undefined;
@@ -143,7 +143,7 @@ export declare function buildMemoryBlockTool(opts: MemoryBlockToolOptions): impo
143
143
  content?: undefined;
144
144
  empty?: undefined;
145
145
  updatedAt?: undefined;
146
- readonly error?: undefined;
146
+ error?: undefined;
147
147
  message?: undefined;
148
148
  removed?: undefined;
149
149
  note?: undefined;
@@ -0,0 +1,11 @@
1
+ import type { PersistentMemoryStore } from './types.js';
2
+ export type PersistentMemoryStoreFactory = () => PersistentMemoryStore | Promise<PersistentMemoryStore>;
3
+ export type PersistentMemoryDurabilityFactory = () => {
4
+ storeA: PersistentMemoryStore;
5
+ storeB: PersistentMemoryStore;
6
+ } | Promise<{
7
+ storeA: PersistentMemoryStore;
8
+ storeB: PersistentMemoryStore;
9
+ }>;
10
+ export declare function runPersistentMemoryStoreContract(factory: PersistentMemoryStoreFactory): void;
11
+ export declare function runPersistentMemoryDurabilityContract(factory: PersistentMemoryDurabilityFactory): void;
@@ -0,0 +1,59 @@
1
+ /// <reference types="bun-types" />
2
+ import { describe, test, expect, beforeEach } from 'bun:test';
3
+ const sampleBlock = (overrides = {}) => ({
4
+ key: 'USER',
5
+ scope: 'user',
6
+ content: 'name: Maya\nprefers: vegetarian',
7
+ charLimit: 1000,
8
+ ...overrides,
9
+ });
10
+ export function runPersistentMemoryStoreContract(factory) {
11
+ describe('PersistentMemoryStore contract', () => {
12
+ let store;
13
+ beforeEach(async () => {
14
+ store = await factory();
15
+ });
16
+ test('loadBlock returns null for missing block', async () => {
17
+ expect(await store.loadBlock('user', 'alice', 'USER')).toBeNull();
18
+ });
19
+ test('saveBlock + loadBlock round-trips', async () => {
20
+ const block = sampleBlock();
21
+ await store.saveBlock(block, 'maya@example.com');
22
+ const loaded = await store.loadBlock('user', 'maya@example.com', 'USER');
23
+ expect(loaded).not.toBeNull();
24
+ expect(loaded.content).toBe(block.content);
25
+ expect(loaded.scope).toBe('user');
26
+ expect(loaded.key).toBe('USER');
27
+ expect(typeof loaded.updatedAt).toBe('string');
28
+ });
29
+ test('listBlocks returns keys within scope+owner', async () => {
30
+ await store.saveBlock(sampleBlock({ key: 'USER' }), 'bob');
31
+ await store.saveBlock(sampleBlock({ key: 'preferences', content: 'dark mode' }), 'bob');
32
+ await store.saveBlock(sampleBlock({ key: 'MEMORY', scope: 'agent', content: 'notes' }), 'bob');
33
+ expect((await store.listBlocks('user', 'bob')).sort()).toEqual([
34
+ 'USER',
35
+ 'preferences',
36
+ ]);
37
+ expect(await store.listBlocks('agent', 'bob')).toEqual(['MEMORY']);
38
+ });
39
+ test('listBlocks returns empty array when none exist', async () => {
40
+ expect(await store.listBlocks('user', 'never-existed')).toEqual([]);
41
+ });
42
+ test('deleteBlock removes block; no-op when missing', async () => {
43
+ await store.saveBlock(sampleBlock({ key: 'ephemeral' }), 'dave');
44
+ await store.deleteBlock('user', 'dave', 'ephemeral');
45
+ expect(await store.loadBlock('user', 'dave', 'ephemeral')).toBeNull();
46
+ await store.deleteBlock('user', 'dave', 'ephemeral');
47
+ });
48
+ });
49
+ }
50
+ export function runPersistentMemoryDurabilityContract(factory) {
51
+ describe('PersistentMemoryStore durability', () => {
52
+ test('store B reads block written by store A', async () => {
53
+ const { storeA, storeB } = await factory();
54
+ await storeA.saveBlock(sampleBlock({ content: 'durable payload' }), 'owner-1');
55
+ const loaded = await storeB.loadBlock('user', 'owner-1', 'USER');
56
+ expect(loaded?.content).toBe('durable payload');
57
+ });
58
+ });
59
+ }