@capekai/core 1.0.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 (148) hide show
  1. package/README.md +12 -0
  2. package/package.json +105 -0
  3. package/src/adapters/ai-sdk.ts +84 -0
  4. package/src/compaction/contracts.ts +82 -0
  5. package/src/compaction/executor.ts +161 -0
  6. package/src/compaction/policy.ts +318 -0
  7. package/src/compaction/recovery.ts +139 -0
  8. package/src/compaction/task.ts +540 -0
  9. package/src/configuration/contracts.ts +58 -0
  10. package/src/configuration/defaults.ts +27 -0
  11. package/src/configuration/runtime.ts +42 -0
  12. package/src/configuration/single-model.ts +75 -0
  13. package/src/context/assembler.ts +112 -0
  14. package/src/context/index.ts +2 -0
  15. package/src/context/sources.ts +119 -0
  16. package/src/context/workspace.ts +63 -0
  17. package/src/core/agent.ts +401 -0
  18. package/src/core/build-tools.ts +139 -0
  19. package/src/core/chat-handler.ts +858 -0
  20. package/src/core/error-handling.ts +18 -0
  21. package/src/core/fork.ts +103 -0
  22. package/src/core/interrupt.ts +192 -0
  23. package/src/core/message-utils.ts +261 -0
  24. package/src/core/model-utils.ts +149 -0
  25. package/src/core/part-utils.ts +88 -0
  26. package/src/core/provider-utils.ts +67 -0
  27. package/src/core/revert.ts +46 -0
  28. package/src/core/step-handlers.ts +157 -0
  29. package/src/core/stream/finalization.ts +65 -0
  30. package/src/core/stream/stream-config.ts +82 -0
  31. package/src/core/stream-handlers.ts +242 -0
  32. package/src/core/structured-output.ts +68 -0
  33. package/src/core/tool-builders/agent-tools.ts +71 -0
  34. package/src/core/tool-builders/external-tools.ts +179 -0
  35. package/src/core/tool-builders/types.ts +16 -0
  36. package/src/core/tool-builders/workspace-tools.ts +293 -0
  37. package/src/core/tool-capabilities.ts +65 -0
  38. package/src/goals/evaluator.ts +171 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/loop.ts +167 -0
  41. package/src/goals/service.ts +39 -0
  42. package/src/index.ts +10 -0
  43. package/src/internal/ask-authority.ts +29 -0
  44. package/src/internal/composition.ts +44 -0
  45. package/src/internal/configuration.ts +22 -0
  46. package/src/internal/execution.ts +108 -0
  47. package/src/internal/hosts.ts +64 -0
  48. package/src/internal/plugins.ts +71 -0
  49. package/src/internal/providers.ts +32 -0
  50. package/src/internal/sandbox.ts +19 -0
  51. package/src/internal/tools.ts +48 -0
  52. package/src/internal/workspace.ts +25 -0
  53. package/src/kernel/diagnostics.ts +249 -0
  54. package/src/kernel/errors.ts +120 -0
  55. package/src/kernel/events.ts +82 -0
  56. package/src/kernel/index.ts +72 -0
  57. package/src/kernel/kernel.ts +62 -0
  58. package/src/kernel/lifecycle.ts +72 -0
  59. package/src/kernel/plugin.ts +218 -0
  60. package/src/kernel/registry.ts +493 -0
  61. package/src/kernel/scope.ts +776 -0
  62. package/src/kernel/service-key.ts +19 -0
  63. package/src/kernel/types.ts +317 -0
  64. package/src/memory/index.ts +2 -0
  65. package/src/memory/memory-tool.ts +75 -0
  66. package/src/memory/registry.ts +172 -0
  67. package/src/permission/ask-user-api.ts +70 -0
  68. package/src/permission/contracts.ts +135 -0
  69. package/src/permission/permission-request-manager.ts +58 -0
  70. package/src/permission/policy.ts +277 -0
  71. package/src/permission/runtime.ts +612 -0
  72. package/src/plugins/compaction-policy.ts +46 -0
  73. package/src/plugins/compose.ts +171 -0
  74. package/src/plugins/context-sections.ts +246 -0
  75. package/src/plugins/default-agent-driver.ts +14 -0
  76. package/src/plugins/facade-plugins.ts +129 -0
  77. package/src/plugins/goal-domain.ts +82 -0
  78. package/src/plugins/legacy-system-message.ts +152 -0
  79. package/src/plugins/loaded-tools.ts +23 -0
  80. package/src/plugins/memory-domain.ts +264 -0
  81. package/src/plugins/orchestrator-session.ts +29 -0
  82. package/src/plugins/permission-policy.ts +49 -0
  83. package/src/plugins/retry-policy.ts +28 -0
  84. package/src/plugins/scheduler-domain.ts +192 -0
  85. package/src/plugins/service-keys.ts +294 -0
  86. package/src/plugins/session-search-domain.ts +238 -0
  87. package/src/plugins/skills-domain.ts +272 -0
  88. package/src/plugins/subagent-domain.ts +287 -0
  89. package/src/plugins/tool-catalog.ts +78 -0
  90. package/src/plugins/tool-output-policy.ts +52 -0
  91. package/src/plugins/value-plugins.ts +150 -0
  92. package/src/plugins/workflow-domain.ts +198 -0
  93. package/src/plugins/workspace-policy.ts +37 -0
  94. package/src/providers/registry.ts +63 -0
  95. package/src/providers/types.ts +44 -0
  96. package/src/retry/policy.ts +282 -0
  97. package/src/retry/stream-chat.ts +312 -0
  98. package/src/runtime/agent-runtime.ts +83 -0
  99. package/src/runtime/default-agent-driver.ts +23 -0
  100. package/src/runtime/domain-tool-source.ts +156 -0
  101. package/src/runtime/events.ts +61 -0
  102. package/src/runtime/host-dependencies.ts +71 -0
  103. package/src/runtime/host-guidance.ts +22 -0
  104. package/src/runtime/host-layout.ts +23 -0
  105. package/src/runtime/host.ts +129 -0
  106. package/src/runtime/standalone-host.ts +118 -0
  107. package/src/sandbox/controller.ts +204 -0
  108. package/src/sandbox/model.ts +305 -0
  109. package/src/sandbox/provider.ts +53 -0
  110. package/src/sandbox/types.ts +110 -0
  111. package/src/scheduler/host.ts +22 -0
  112. package/src/scheduler/scheduler-tool.ts +172 -0
  113. package/src/session-search/host.ts +56 -0
  114. package/src/session-search/index.ts +23 -0
  115. package/src/session-search/session-search-tool.ts +151 -0
  116. package/src/skills/index.ts +3 -0
  117. package/src/skills/registry.ts +63 -0
  118. package/src/skills/skill-manage-tool.ts +205 -0
  119. package/src/skills/skill-tool.ts +42 -0
  120. package/src/storage/contracts.ts +159 -0
  121. package/src/storage/memory.ts +321 -0
  122. package/src/storage/options.ts +75 -0
  123. package/src/storage/runtime.ts +115 -0
  124. package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
  125. package/src/storage/sqlite.ts +321 -0
  126. package/src/storage/tool-output-artifacts.ts +75 -0
  127. package/src/storage.ts +31 -0
  128. package/src/subagent/child-session.ts +282 -0
  129. package/src/subagent/guidance.ts +8 -0
  130. package/src/subagent/policy.ts +198 -0
  131. package/src/subagent/task-tool.ts +584 -0
  132. package/src/tool-output/contracts.ts +111 -0
  133. package/src/tool-output/policy.ts +410 -0
  134. package/src/tool.ts +1 -0
  135. package/src/tools/executor.ts +258 -0
  136. package/src/tools/install-manifest.ts +40 -0
  137. package/src/tools/llm-api.ts +77 -0
  138. package/src/tools/registry.ts +206 -0
  139. package/src/tools/tool-artifact.ts +182 -0
  140. package/src/tools/tool-source.ts +53 -0
  141. package/src/utils/errors.ts +334 -0
  142. package/src/utils/strip-visualization.ts +50 -0
  143. package/src/workflow/decomposer.ts +139 -0
  144. package/src/workflow/execution.ts +523 -0
  145. package/src/workflow/orchestrator-session.ts +161 -0
  146. package/src/workflow/synthesizer.ts +130 -0
  147. package/src/workspace/contracts.ts +135 -0
  148. package/src/workspace/policy.ts +327 -0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Typed service keys. Keys are frozen, compared by id, and carry a phantom
3
+ * type so require/optional return the provider's contract type.
4
+ */
5
+
6
+ import { MalformedPluginError } from './errors';
7
+ import type { RuntimeScope, ServiceKey } from './types';
8
+
9
+ const VALID_SCOPES = new Set<string>(['process', 'agent', 'run']);
10
+
11
+ export function serviceKey<T = unknown>(id: string, scope: RuntimeScope): ServiceKey<T> {
12
+ if (typeof id !== 'string' || id.length === 0) {
13
+ throw new MalformedPluginError('service key id must be a non-empty string');
14
+ }
15
+ if (!VALID_SCOPES.has(scope)) {
16
+ throw new MalformedPluginError(`service key '${id}' has invalid scope '${String(scope)}'`);
17
+ }
18
+ return Object.freeze({ id, scope });
19
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Kernel public types.
3
+ *
4
+ * The kernel is an internal dependency-free composition layer. It must not
5
+ * import the AI SDK, Jean2 packages, Hono, SQLite, Bun product APIs, or any
6
+ * Capek product domain. See 04-plugin-contract.md for the guarantees these
7
+ * types describe.
8
+ */
9
+
10
+ export type RuntimeScope = 'process' | 'agent' | 'run';
11
+
12
+ /** A typed capability contract. Identity is the id; scope names where the
13
+ * service may be provided. */
14
+ export interface ServiceKey<T = unknown> {
15
+ readonly id: string;
16
+ readonly scope: RuntimeScope;
17
+ readonly _type?: T;
18
+ }
19
+
20
+ export interface Disposable {
21
+ dispose(): void | Promise<void>;
22
+ }
23
+
24
+ /** A completion or persistence barrier awaited during scope disposal. */
25
+ export type CleanupBarrier = PromiseLike<void> | (() => PromiseLike<void>);
26
+
27
+ /** Explicitly replaces the provider named by replacedProvider for one key. */
28
+ export interface ServiceOverride {
29
+ readonly key: ServiceKey<unknown>;
30
+ readonly replacedProvider: string;
31
+ }
32
+
33
+ /** A lifecycle unit that declares dependencies and contributes behavior. */
34
+ export interface CapekPlugin<Options = unknown> {
35
+ readonly id: string;
36
+ readonly version?: string;
37
+ readonly scope: RuntimeScope;
38
+ readonly provides?: readonly ServiceKey<unknown>[];
39
+ readonly requires?: readonly ServiceKey<unknown>[];
40
+ readonly optional?: readonly ServiceKey<unknown>[];
41
+ readonly overrides?: readonly ServiceOverride[];
42
+ setup(
43
+ context: PluginContext,
44
+ options: Options,
45
+ ): void | Disposable | Promise<void | Disposable>;
46
+ }
47
+
48
+ /** An opaque model-facing tool payload. The kernel validates only that
49
+ * `name` is a non-empty string; `parameters`, `inputSchema`, and any
50
+ * additional fields are carried through untouched. */
51
+ export interface ToolDefinition {
52
+ readonly name: string;
53
+ readonly description?: string;
54
+ readonly parameters?: Readonly<Record<string, unknown>>;
55
+ readonly inputSchema?: Readonly<Record<string, unknown>>;
56
+ readonly [extra: string]: unknown;
57
+ }
58
+
59
+ /** Optional visibility metadata for a tool. An explicit false hides the
60
+ * tool even when every required capability resolves; reason explains why. */
61
+ export interface ToolVisibility {
62
+ readonly visible: boolean;
63
+ readonly reason?: string;
64
+ }
65
+
66
+ /** A model-facing tool contribution. Effective visibility combines an
67
+ * explicit visibility false with missing required capabilities, matched by
68
+ * service id and ServiceKey scope. Order is optional; omitted orders
69
+ * default to 0 and sort before explicitly ordered tools with plugin-id
70
+ * then contribution-id tie-breaks. */
71
+ export interface ToolContribution {
72
+ readonly id: string;
73
+ readonly order?: number;
74
+ readonly definition: ToolDefinition;
75
+ /** Opaque product-layer payload (for capek: the exact `LoadedTool` this
76
+ * contribution advertises). The kernel never reads it; the effective-tool
77
+ * view carries it through unchanged. */
78
+ readonly payload?: unknown;
79
+ readonly requiredCapabilities?: readonly ServiceKey<unknown>[];
80
+ readonly visibility?: ToolVisibility;
81
+ }
82
+
83
+ export type ContextPhase =
84
+ | 'identity'
85
+ | 'preferences'
86
+ | 'instructions'
87
+ | 'workspace'
88
+ | 'capabilities'
89
+ | 'task';
90
+
91
+ /** The build-time context handed to a section provider. `data` carries the
92
+ * opaque assembly options the caller passed to `buildContext(data?)`; the
93
+ * kernel validates only that it is an object when present and never reads
94
+ * its fields, so it stays dependency-free. Product layers type the data
95
+ * through the `TData` parameter and validate the concrete shape. */
96
+ export interface ContextBuildContext<TData = unknown> {
97
+ readonly kind: RuntimeScope;
98
+ readonly data?: TData;
99
+ }
100
+
101
+ /** A context section contribution. Null omits the section without changing
102
+ * the ordering of the others. */
103
+ export interface ContextSectionContribution<TData = unknown> {
104
+ readonly id: string;
105
+ readonly phase: ContextPhase;
106
+ readonly order: number;
107
+ provide(context: ContextBuildContext<TData>): string | null | Promise<string | null>;
108
+ }
109
+
110
+ export type RunTerminalOutcome = 'completed' | 'failed' | 'cancelled';
111
+
112
+ export interface RunStartedEvent {
113
+ readonly type: 'run:started';
114
+ readonly runId: string;
115
+ }
116
+
117
+ export interface RunTerminalEvent {
118
+ readonly type: 'run:terminal';
119
+ readonly runId: string;
120
+ readonly outcome: RunTerminalOutcome;
121
+ readonly reason?: string;
122
+ }
123
+
124
+ export interface RunDisposedEvent {
125
+ readonly type: 'run:disposed';
126
+ readonly runId: string;
127
+ }
128
+
129
+ export interface KernelEventMap {
130
+ 'run:started': RunStartedEvent;
131
+ 'run:terminal': RunTerminalEvent;
132
+ 'run:disposed': RunDisposedEvent;
133
+ }
134
+
135
+ export type KernelEventType = keyof KernelEventMap;
136
+ export type KernelEvent = KernelEventMap[KernelEventType];
137
+
138
+ /** An observer contribution for typed runtime events. Listeners are awaited
139
+ * in deterministic order during emit. */
140
+ export interface EventListenerContribution {
141
+ readonly id: string;
142
+ readonly eventTypes: readonly KernelEventType[];
143
+ handle(event: KernelEvent): void | Promise<void>;
144
+ }
145
+
146
+
147
+ export type ScopeStatus = 'active' | 'disposing' | 'disposed';
148
+ export type PluginStatus = 'pending' | 'active' | 'failed' | 'disposed';
149
+ export type RunStatus = 'created' | 'running' | 'terminal' | 'disposed';
150
+
151
+ export interface PluginDiagnostic {
152
+ readonly id: string;
153
+ readonly version?: string;
154
+ readonly scope: RuntimeScope;
155
+ readonly status: PluginStatus;
156
+ }
157
+
158
+ export interface ServiceDiagnostic {
159
+ readonly keyId: string;
160
+ readonly keyScope: RuntimeScope;
161
+ readonly providerPluginId: string;
162
+ readonly providerScope: RuntimeScope;
163
+ }
164
+
165
+ export interface ToolDiagnostic {
166
+ readonly id: string;
167
+ readonly order: number;
168
+ readonly pluginId: string;
169
+ readonly visible: boolean;
170
+ readonly hiddenReasons: readonly string[];
171
+ }
172
+
173
+ export interface ContextSectionDiagnostic {
174
+ readonly id: string;
175
+ readonly phase: ContextPhase;
176
+ readonly order: number;
177
+ readonly pluginId: string;
178
+ readonly scopeKind: RuntimeScope;
179
+ }
180
+
181
+ export interface ListenerDiagnostic {
182
+ readonly id: string;
183
+ readonly eventTypes: readonly KernelEventType[];
184
+ readonly pluginId: string;
185
+ readonly scopeKind: RuntimeScope;
186
+ }
187
+
188
+
189
+ /** Read-only composition inventory. Never contains plugin options or service
190
+ * values, so it is safe to surface to support and tests. */
191
+ export interface ScopeDiagnosticsSnapshot {
192
+ readonly scopeId: string;
193
+ readonly kind: RuntimeScope;
194
+ readonly parentKind: RuntimeScope | null;
195
+ readonly status: ScopeStatus;
196
+ readonly plugins: readonly PluginDiagnostic[];
197
+ readonly services: readonly ServiceDiagnostic[];
198
+ readonly tools: readonly ToolDiagnostic[];
199
+ readonly contextSections: readonly ContextSectionDiagnostic[];
200
+ readonly listeners: readonly ListenerDiagnostic[];
201
+ readonly runId?: string;
202
+ readonly runStatus?: RunStatus;
203
+ readonly runOutcome?: RunTerminalOutcome;
204
+ readonly cleanupBarrierCount: number;
205
+ }
206
+
207
+ export interface CompositionDiagnostics {
208
+ snapshot(): ScopeDiagnosticsSnapshot;
209
+ }
210
+
211
+ /** The setup-time surface handed to plugins. Registrations return disposers
212
+ * owned by the installing scope. */
213
+ export interface PluginContext {
214
+ readonly kind: RuntimeScope;
215
+ readonly scopeId: string;
216
+ provide<T>(key: ServiceKey<T>, service: T): Disposable;
217
+ require<T>(key: ServiceKey<T>): T;
218
+ optional<T>(key: ServiceKey<T>): T | undefined;
219
+ contributeTool(contribution: ToolContribution): Disposable;
220
+ contributeContext(contribution: ContextSectionContribution): Disposable;
221
+ /** The effective tool contributions of the current scope chain in
222
+ * deterministic order, including contributions registered earlier in
223
+ * setup. Narrow like buildContext: plugins never receive the scope
224
+ * handle just to inspect tools. */
225
+ listTools(): readonly EffectiveTool[];
226
+ /** Assembles the effective context sections of the current scope chain in
227
+ * deterministic order, including sections registered earlier in setup.
228
+ * Narrow by design: plugins never receive the scope handle just to build
229
+ * context. The optional `data` is passed through to section providers as
230
+ * `ContextBuildContext.data` after the kernel validates it is an object. */
231
+ buildContext<TData = unknown>(data?: TData): Promise<readonly ProvidedContextSection[]>;
232
+ contributeListener(contribution: EventListenerContribution): Disposable;
233
+ registerCleanupBarrier(barrier: CleanupBarrier): Disposable;
234
+ readonly diagnostics: CompositionDiagnostics;
235
+ }
236
+
237
+ export interface EffectiveTool {
238
+ readonly id: string;
239
+ readonly order: number;
240
+ readonly definition: ToolDefinition;
241
+ /** The contribution's opaque payload, carried through unchanged. */
242
+ readonly payload?: unknown;
243
+ readonly pluginId: string;
244
+ readonly visible: boolean;
245
+ readonly hiddenReasons: readonly string[];
246
+ }
247
+
248
+ export interface EffectiveContextSection {
249
+ readonly id: string;
250
+ readonly phase: ContextPhase;
251
+ readonly order: number;
252
+ readonly pluginId: string;
253
+ readonly scopeKind: RuntimeScope;
254
+ }
255
+
256
+ export interface ProvidedContextSection {
257
+ readonly id: string;
258
+ readonly phase: ContextPhase;
259
+ readonly content: string;
260
+ }
261
+
262
+ export interface ScopeHandle {
263
+ readonly kind: RuntimeScope;
264
+ readonly scopeId: string;
265
+ readonly parent: ScopeHandle | null;
266
+ /** Live child scopes. A disposed child unregisters from its parent, so
267
+ * this never retains closed scopes. */
268
+ readonly childCount: number;
269
+ require<T>(key: ServiceKey<T>): T;
270
+ optional<T>(key: ServiceKey<T>): T | undefined;
271
+ snapshot(): ScopeDiagnosticsSnapshot;
272
+ listTools(): readonly EffectiveTool[];
273
+ listContextSections(): readonly EffectiveContextSection[];
274
+ /** Assembles effective context sections in deterministic order. Null
275
+ * sections are omitted without shifting the others. The optional `data`
276
+ * must be an object when present and is passed through to every section
277
+ * provider as `ContextBuildContext.data`. */
278
+ buildContext<TData = unknown>(data?: TData): Promise<readonly ProvidedContextSection[]>;
279
+ dispose(): Promise<void>;
280
+ }
281
+
282
+ export interface RunCancellation {
283
+ /** True when this call moved the run from a live state to terminal. */
284
+ readonly acknowledged: boolean;
285
+ /** Resolves after terminal event dispatch, cleanup barriers, reverse
286
+ * disposal, and the disposed event. */
287
+ readonly completion: Promise<void>;
288
+ }
289
+
290
+ export interface ProcessScopeHandle extends ScopeHandle {
291
+ readonly kind: 'process';
292
+ createAgentScope(
293
+ plugins: readonly CapekPlugin[],
294
+ options?: PluginOptionsMap,
295
+ ): Promise<AgentScopeHandle>;
296
+ }
297
+
298
+ export interface AgentScopeHandle extends ScopeHandle {
299
+ readonly kind: 'agent';
300
+ createRunScope(
301
+ runId: string,
302
+ plugins: readonly CapekPlugin[],
303
+ options?: PluginOptionsMap,
304
+ ): Promise<RunScopeHandle>;
305
+ }
306
+
307
+ export interface RunScopeHandle extends ScopeHandle {
308
+ readonly kind: 'run';
309
+ readonly runId: string;
310
+ readonly runStatus: RunStatus;
311
+ start(): Promise<void>;
312
+ markTerminal(outcome: 'completed' | 'failed'): Promise<void>;
313
+ cancel(reason?: string): RunCancellation;
314
+ registerCleanupBarrier(barrier: CleanupBarrier): Disposable;
315
+ }
316
+
317
+ export type PluginOptionsMap = Readonly<Record<string, unknown>>;
@@ -0,0 +1,2 @@
1
+ export * from './registry';
2
+ export * from './memory-tool';
@@ -0,0 +1,75 @@
1
+ import type { PermissionAsk, PermissionRiskLevel } from '@capekai/tool';
2
+ import {
3
+ addEntry,
4
+ listEntries,
5
+ removeEntry,
6
+ replaceEntry,
7
+ MEMORY_LINE_MEMORY_TARGET,
8
+ MEMORY_LINE_NO_SECRETS,
9
+ MEMORY_LINE_ONLY_COMPACT,
10
+ MEMORY_LINE_USE_LIST,
11
+ MEMORY_LINE_USER_TARGET,
12
+ type MemoryActionResult,
13
+ type MemoryTarget,
14
+ } from './registry';
15
+
16
+ export const memoryToolDefinition = {
17
+ name: 'memory',
18
+ description: `Persist durable workspace knowledge across sessions.
19
+
20
+ ${MEMORY_LINE_USER_TARGET}
21
+ ${MEMORY_LINE_MEMORY_TARGET}
22
+
23
+ Character limits: user=1500 chars, workspace=2500 chars. Keep entries compact.
24
+
25
+ Actions:
26
+ - list: Read current entries and char usage for a target. Requires target only.
27
+ - add: Append a new bullet entry. Requires content.
28
+ - replace: Find an entry by oldText substring and replace it. Requires oldText and content.
29
+ - remove: Find an entry by oldText substring and remove it. Requires oldText.
30
+
31
+ ${MEMORY_LINE_USE_LIST}
32
+ ${MEMORY_LINE_ONLY_COMPACT}
33
+ ${MEMORY_LINE_NO_SECRETS}`,
34
+ inputSchema: {
35
+ type: 'object' as const,
36
+ properties: {
37
+ action: { type: 'string' as const, enum: ['list', 'add', 'replace', 'remove'], description: 'The action to perform on the memory file.' },
38
+ target: { type: 'string' as const, enum: ['user', 'memory'], description: 'Which memory file to modify. "user" for preferences, "memory" for workspace facts.' },
39
+ content: { type: 'string' as const, description: 'The new content for add/replace actions.' },
40
+ oldText: { type: 'string' as const, description: 'The text to find for replace/remove actions. Must match exactly one entry.' },
41
+ },
42
+ required: ['action', 'target'],
43
+ },
44
+ timeout: 10000,
45
+ };
46
+
47
+ export async function executeMemoryTool(input: Record<string, unknown>, basePath: string, risk: PermissionRiskLevel, askFn?: (ask: PermissionAsk) => Promise<unknown>): Promise<MemoryActionResult> {
48
+ const action = input.action as 'list' | 'add' | 'replace' | 'remove';
49
+ const target = input.target as MemoryTarget;
50
+ const content = input.content as string | undefined;
51
+ const oldText = input.oldText as string | undefined;
52
+ if (!['list', 'add', 'replace', 'remove'].includes(action)) return { success: false, error: 'Invalid action. Must be list, add, replace, or remove.' };
53
+ if (!['user', 'memory'].includes(target)) return { success: false, error: 'Invalid target. Must be user or memory.' };
54
+ if (action === 'list') return listEntries(basePath, target);
55
+ if ((action === 'add' || action === 'replace') && typeof content !== 'string') {
56
+ return { success: false, error: `Content is required for ${action} action.` };
57
+ }
58
+ if ((action === 'replace' || action === 'remove') && typeof oldText !== 'string') {
59
+ return { success: false, error: `oldText is required for ${action} action.` };
60
+ }
61
+ if (risk !== 'none' && askFn) {
62
+ const approved = await askFn({
63
+ type: 'permission', question: `Allow memory ${action} on ${target}?`,
64
+ description: `Action: ${action}\nTarget: ${target}${content ? `\nContent: ${content.slice(0, 200)}` : ''}${oldText ? `\nOld text: ${oldText.slice(0, 200)}` : ''}`,
65
+ risk, resource: 'file', action: 'write', paths: [target === 'user' ? 'USER.md' : 'MEMORY.md'],
66
+ });
67
+ if (!approved) return { success: false, error: 'USER_REJECTION' };
68
+ }
69
+ if (action === 'add') return content ? addEntry(basePath, target, content) : { success: false, error: 'Content is required for add action.' };
70
+ if (action === 'replace') {
71
+ if (!oldText) return { success: false, error: 'oldText is required for replace action.' };
72
+ return content ? replaceEntry(basePath, target, oldText, content) : { success: false, error: 'Content is required for replace action.' };
73
+ }
74
+ return oldText ? removeEntry(basePath, target, oldText) : { success: false, error: 'oldText is required for remove action.' };
75
+ }
@@ -0,0 +1,172 @@
1
+ import { existsSync } from 'fs';
2
+ import { mkdir, readFile, writeFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+
5
+ const USER_FILE = 'USER.md';
6
+ const MEMORY_FILE = 'MEMORY.md';
7
+ export const USER_CHAR_LIMIT = 1500;
8
+ export const MEMORY_CHAR_LIMIT = 2500;
9
+ export type MemoryTarget = 'user' | 'memory';
10
+
11
+ export interface MemoryUsage { chars: number; limit: number }
12
+ export interface MemoryFile {
13
+ path: string;
14
+ content: string;
15
+ entries: string[];
16
+ charCount: number;
17
+ charLimit: number;
18
+ }
19
+ export interface MemoryActionResult {
20
+ success: boolean;
21
+ result?: {
22
+ target: MemoryTarget;
23
+ action: 'list' | 'add' | 'replace' | 'remove';
24
+ path: string;
25
+ usage: MemoryUsage;
26
+ entry?: string;
27
+ entries?: string[];
28
+ };
29
+ error?: string;
30
+ entries?: string[];
31
+ usage?: MemoryUsage;
32
+ }
33
+
34
+ const fileName = (target: MemoryTarget) => target === 'user' ? USER_FILE : MEMORY_FILE;
35
+ const filePath = (basePath: string, target: MemoryTarget) => join(basePath, fileName(target));
36
+ const charLimit = (target: MemoryTarget) => target === 'user' ? USER_CHAR_LIMIT : MEMORY_CHAR_LIMIT;
37
+
38
+ export function parseEntries(content: string): string[] {
39
+ return content.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
40
+ }
41
+ export function entriesToContent(entries: string[]): string { return entries.join('\n'); }
42
+ export function formatEntriesForDisplay(entries: string[]): string[] {
43
+ return entries.map((entry, index) => `[${index}] ${entry.replace(/^- /, '')}`);
44
+ }
45
+ export function formatMemorySection(tag: string, path: string, content: string, chars: number, limit: number): string {
46
+ return `<${tag} path="${path}" usage="${chars}/${limit}">\n${content}\n</${tag}>`;
47
+ }
48
+
49
+ export async function loadMemoryFile(basePath: string, target: MemoryTarget): Promise<MemoryFile | null> {
50
+ const path = filePath(basePath, target);
51
+ if (!existsSync(path)) return null;
52
+ try {
53
+ const content = (await readFile(path, 'utf-8')).trim();
54
+ if (!content) return null;
55
+ return { path: fileName(target), content, entries: parseEntries(content), charCount: content.length, charLimit: charLimit(target) };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ async function ensureDir(basePath: string): Promise<void> {
62
+ if (!existsSync(basePath)) await mkdir(basePath, { recursive: true });
63
+ }
64
+
65
+ function fullResult(existing: string, target: MemoryTarget, entries: string[], replaceHint: string): MemoryActionResult {
66
+ const limit = charLimit(target);
67
+ return {
68
+ success: false,
69
+ error: `Memory is full (${existing.length}/${limit} chars). Consider merging related entries to free space, or ${replaceHint}.`,
70
+ entries: formatEntriesForDisplay(entries),
71
+ usage: { chars: existing.length, limit },
72
+ };
73
+ }
74
+
75
+ export async function addEntry(basePath: string, target: MemoryTarget, content: string): Promise<MemoryActionResult> {
76
+ const trimmed = content.trim();
77
+ if (!trimmed) return { success: false, error: 'Content cannot be empty.' };
78
+ const entry = `- ${trimmed}`;
79
+ const path = filePath(basePath, target);
80
+ let existingContent = '';
81
+ let entries: string[] = [];
82
+ if (existsSync(path)) {
83
+ try {
84
+ existingContent = (await readFile(path, 'utf-8')).trim();
85
+ entries = parseEntries(existingContent);
86
+ } catch {
87
+ existingContent = '';
88
+ entries = [];
89
+ }
90
+ }
91
+ if (entries.includes(entry)) return { success: false, error: 'Exact duplicate entry already exists.' };
92
+ const next = existingContent ? `${existingContent}\n${entry}` : entry;
93
+ if (next.length > charLimit(target)) return fullResult(existingContent, target, entries, 'replace/remove existing ones first');
94
+ await ensureDir(basePath);
95
+ await writeFile(path, next, 'utf-8');
96
+ return { success: true, result: { target, action: 'add', path: fileName(target), usage: { chars: next.length, limit: charLimit(target) }, entry: trimmed } };
97
+ }
98
+
99
+ async function readExisting(basePath: string, target: MemoryTarget): Promise<{ content: string; entries: string[] } | MemoryActionResult> {
100
+ const path = filePath(basePath, target);
101
+ if (!existsSync(path)) return { success: false, error: 'Memory file does not exist.' };
102
+ try {
103
+ const content = (await readFile(path, 'utf-8')).trim();
104
+ return { content, entries: parseEntries(content) };
105
+ } catch {
106
+ return { success: false, error: 'Failed to read memory file.' };
107
+ }
108
+ }
109
+
110
+ function findOne(content: string, entries: string[], target: MemoryTarget, oldText: string): string | MemoryActionResult {
111
+ const matches = entries.filter((entry) => entry.includes(oldText));
112
+ const usage = { chars: content.length, limit: charLimit(target) };
113
+ if (matches.length === 0) return { success: false, error: `No entry found matching "${oldText}". Use the list action to see current entries.`, entries: formatEntriesForDisplay(entries), usage };
114
+ if (matches.length > 1) return { success: false, error: `Multiple entries match "${oldText}". Be more specific.`, entries: formatEntriesForDisplay(matches), usage };
115
+ return matches[0];
116
+ }
117
+
118
+ export async function replaceEntry(basePath: string, target: MemoryTarget, oldText: string, content: string): Promise<MemoryActionResult> {
119
+ const trimmed = content.trim();
120
+ if (!trimmed) return { success: false, error: 'New content cannot be empty.' };
121
+ const loaded = await readExisting(basePath, target);
122
+ if ('success' in loaded) return loaded;
123
+ const match = findOne(loaded.content, loaded.entries, target, oldText);
124
+ if (typeof match !== 'string') return match;
125
+ const next = loaded.content.replace(match, `- ${trimmed}`);
126
+ if (next.length > charLimit(target)) return fullResult(loaded.content, target, loaded.entries, 'remove existing ones first');
127
+ await writeFile(filePath(basePath, target), next, 'utf-8');
128
+ return { success: true, result: { target, action: 'replace', path: fileName(target), usage: { chars: next.length, limit: charLimit(target) }, entry: trimmed } };
129
+ }
130
+
131
+ export async function removeEntry(basePath: string, target: MemoryTarget, oldText: string): Promise<MemoryActionResult> {
132
+ const loaded = await readExisting(basePath, target);
133
+ if ('success' in loaded) return loaded;
134
+ const match = findOne(loaded.content, loaded.entries, target, oldText);
135
+ if (typeof match !== 'string') return match;
136
+ const lines = loaded.content.split('\n');
137
+ const matchIndex = lines.findIndex((line) => line.trim() === match);
138
+ const next = matchIndex >= 0
139
+ ? [...lines.slice(0, matchIndex), ...lines.slice(matchIndex + 1)].join('\n').trim()
140
+ : loaded.content;
141
+ await writeFile(filePath(basePath, target), next, 'utf-8');
142
+ return { success: true, result: { target, action: 'remove', path: fileName(target), usage: { chars: next.length, limit: charLimit(target) } } };
143
+ }
144
+
145
+ export async function listEntries(basePath: string, target: MemoryTarget): Promise<MemoryActionResult> {
146
+ const file = await loadMemoryFile(basePath, target);
147
+ return { success: true, result: { target, action: 'list', path: fileName(target), usage: { chars: file?.charCount ?? 0, limit: charLimit(target) }, entries: file ? formatEntriesForDisplay(file.entries) : [] } };
148
+ }
149
+
150
+ export async function loadMemoryInstructions(basePath: string): Promise<string | null> {
151
+ const sections: string[] = [];
152
+ const user = await loadMemoryFile(basePath, 'user');
153
+ if (user) sections.push(formatMemorySection('user_memory', user.path, user.content, user.charCount, user.charLimit));
154
+ const memory = await loadMemoryFile(basePath, 'memory');
155
+ if (memory) sections.push(formatMemorySection('workspace_memory', memory.path, memory.content, memory.charCount, memory.charLimit));
156
+ return sections.length > 0 ? sections.join('\n\n') : null;
157
+ }
158
+
159
+ export const MEMORY_LINE_USER_TARGET = 'Use target="user" for user preferences and communication/workflow expectations.';
160
+ export const MEMORY_LINE_MEMORY_TARGET = 'Use target="memory" for workspace facts, repo conventions, commands, lessons, and non-obvious fixes.';
161
+ export const MEMORY_LINE_ONLY_COMPACT = 'Only save compact facts that should affect future sessions.';
162
+ export const MEMORY_LINE_NO_SECRETS = 'Do not save secrets, raw logs, large code, or one-off details.';
163
+ export const MEMORY_LINE_USE_LIST = 'Use list before replace/remove to see the exact current entries and avoid guesswork.';
164
+
165
+ export const MEMORY_GUIDANCE = `You can persist durable workspace knowledge using the memory tool.
166
+ ${MEMORY_LINE_USER_TARGET}
167
+ ${MEMORY_LINE_MEMORY_TARGET}
168
+ Character limits: user=${USER_CHAR_LIMIT}, workspace=${MEMORY_CHAR_LIMIT}.
169
+ ${MEMORY_LINE_ONLY_COMPACT}
170
+ ${MEMORY_LINE_NO_SECRETS}
171
+ If memory is full, consolidate existing entries with replace before adding.
172
+ Use the list action to verify current entries before replacing or removing.`;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * C6 generic ask surface over the NON-REPLACEABLE permission runtime.
3
+ * These exports preserve the exact pre-C6 `tools/ask-user-api.ts`
4
+ * identities; the state and decisions resolve through
5
+ * `getPermissionRuntimeService()`, so a composed agent scope owns its
6
+ * pending asks and timers while unscoped consumers keep the process-default
7
+ * behavior.
8
+ */
9
+
10
+ import type { AskApi } from '@capekai/tool'
11
+ import type { AskAuthority } from '@capekai/types';
12
+ import type { PendingAskRecord } from '../runtime/host';
13
+ import { ASK_TIMEOUT } from './policy';
14
+ import { getPermissionRuntimeService } from './runtime';
15
+ import type { AskBroadcastFn } from './contracts';
16
+
17
+ export { ASK_TIMEOUT };
18
+ export type { AskBroadcastFn };
19
+
20
+ export function createAskApi(
21
+ sessionId: string,
22
+ toolCallId: string,
23
+ toolName: string,
24
+ broadcastFn: AskBroadcastFn,
25
+ workspaceId?: string,
26
+ rootSessionId?: string,
27
+ ): AskApi {
28
+ return getPermissionRuntimeService().createAskApi(
29
+ sessionId,
30
+ toolCallId,
31
+ toolName,
32
+ broadcastFn,
33
+ workspaceId,
34
+ rootSessionId,
35
+ );
36
+ }
37
+
38
+ export async function resolveAsk(toolCallId: string, response: unknown, requestId?: string): Promise<boolean> {
39
+ return getPermissionRuntimeService().resolveAsk(toolCallId, response, requestId);
40
+ }
41
+
42
+ export async function rejectAsk(toolCallId: string, error: Error): Promise<boolean> {
43
+ return getPermissionRuntimeService().rejectAsk(toolCallId, error);
44
+ }
45
+
46
+ export async function rejectPendingAsksByToolCallId(toolCallId: string, error?: Error): Promise<string[]> {
47
+ return getPermissionRuntimeService().rejectPendingAsksByToolCallId(toolCallId, error);
48
+ }
49
+
50
+ export async function rejectPendingAsksBySession(sessionId: string, error?: Error): Promise<string[]> {
51
+ return getPermissionRuntimeService().rejectPendingAsksBySession(sessionId, error);
52
+ }
53
+
54
+ export function hasPendingAsk(toolCallId: string): boolean {
55
+ return getPermissionRuntimeService().hasPendingAsk(toolCallId);
56
+ }
57
+
58
+ export function getAuthorityForPendingAsk(toolCallId: string): AskAuthority | undefined {
59
+ return getPermissionRuntimeService().getAuthorityForPendingAsk(toolCallId);
60
+ }
61
+
62
+ export async function getSessionIdForPendingAsk(toolCallId: string, requestId?: string): Promise<string | null> {
63
+ return getPermissionRuntimeService().getSessionIdForPendingAsk(toolCallId, requestId);
64
+ }
65
+
66
+ export const listPendingAsksBySession = (sessionId: string): Promise<PendingAskRecord[]> =>
67
+ getPermissionRuntimeService().listPendingAsksBySession(sessionId);
68
+
69
+ export const listPendingAsksByRootSession = (rootSessionId: string): Promise<PendingAskRecord[]> =>
70
+ getPermissionRuntimeService().listPendingAsksByRootSession(rootSessionId);