@a5c-ai/krate 5.0.1-staging.660d2b90f → 5.0.1-staging.69cb593ea
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/Dockerfile +31 -29
- package/bin/krate-demo.mjs +0 -0
- package/bin/krate-server.mjs +0 -0
- package/dist/krate-controller-ui.json +808 -10
- package/dist/krate-lifecycle.json +1 -1
- package/dist/krate-runtime-snapshot.json +223 -53
- package/dist/krate-summary.json +40 -3
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/architecture-v2.md +431 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/requirements-v2.md +238 -0
- package/docs/sdk-api-reference.md +782 -0
- package/docs/system-spec-v2.md +352 -0
- package/docs/todos.md +4 -0
- package/docs/web-console-spec.md +433 -0
- package/package.json +1 -1
- package/scripts/validate-ui.mjs +305 -207
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +47 -0
- package/src/agent-dispatch-controller.js +130 -7
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +1 -1
- package/src/agent-permission-review.js +102 -14
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +52 -1
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +273 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +338 -3
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +48 -6
- package/src/controller-client.js +112 -38
- package/src/controller-ui.js +96 -16
- package/src/data-plane.js +3 -2
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +36 -0
- package/src/gitea-service.js +173 -0
- package/src/http-server.js +226 -0
- package/src/index.js +27 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +156 -84
- package/src/notification-controller.js +178 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +26 -8
- package/src/runner-controller.js +272 -0
- package/src/snapshot-cache.js +157 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-dispatch-controller.test.js +139 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +35 -19
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +43 -29
- package/tests/e2e/lifecycle.test.js +5 -2
- package/tests/event-bus-integration.test.js +190 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +58 -6
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createResource, clone } from './resource-model.js';
|
|
3
|
+
import { queryGraph, queryGrep, queryMemory } from './agent-memory-query.js';
|
|
4
|
+
|
|
5
|
+
export const AGENT_MEMORY_CONTROLLER_BOUNDARY = {
|
|
6
|
+
role: 'agent-memory-controller',
|
|
7
|
+
scope: 'Company Brain memory management — search, snapshots, redaction, imports, time-travel',
|
|
8
|
+
owns: ['memory search', 'snapshot pinning', 'redaction scanning', 'import lifecycle', 'ontology validation'],
|
|
9
|
+
delegatesTo: ['resource-model'],
|
|
10
|
+
mustNotOwn: ['git operations', 'secret values', 'Agent Mux sessions']
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Redaction patterns (same as agent-context-bundles)
|
|
14
|
+
const REDACTION_PATTERNS = [
|
|
15
|
+
{ kind: 'secret-key', pattern: /(?:API_KEY|API_SECRET|SECRET_KEY|ACCESS_KEY|PRIVATE_KEY|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)\s*[=:]\s*['"]?([^\s'"}{,\]]+)/gi },
|
|
16
|
+
{ kind: 'provider-token', pattern: /\b(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36,}|gho_[a-zA-Z0-9]{36,}|glpat-[a-zA-Z0-9\-_]{20,}|xoxb-[a-zA-Z0-9\-]+|xoxp-[a-zA-Z0-9\-]+)\b/g },
|
|
17
|
+
{ kind: 'bearer-token', pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/gi },
|
|
18
|
+
{ kind: 'private-key', pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g },
|
|
19
|
+
{ kind: 'base64-credential', pattern: /\b[A-Za-z0-9+\/]{40,}={0,2}\b/g },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function sha256(data) {
|
|
23
|
+
return createHash('sha256').update(data).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const IMPORT_PHASES = ['Pending', 'Collecting', 'Redacting', 'Normalizing', 'Validating', 'AwaitingReview'];
|
|
27
|
+
|
|
28
|
+
export function createAgentMemoryController(options = {}) {
|
|
29
|
+
return {
|
|
30
|
+
role: 'agent-memory-controller',
|
|
31
|
+
|
|
32
|
+
createMemorySnapshot({ memoryRepository, requestedRef, resolvedCommit, queryManifest, selectedRecords, selectedDocuments, ontologyDigest, namespace = 'default', organizationRef = 'default' }) {
|
|
33
|
+
const queryManifestDigest = sha256(JSON.stringify(queryManifest || {}));
|
|
34
|
+
const selectedRecordsDigest = sha256(JSON.stringify(selectedRecords || []));
|
|
35
|
+
const selectedDocumentsDigest = sha256(JSON.stringify(selectedDocuments || []));
|
|
36
|
+
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
const snapshotName = `memsnapshot-${sha256(memoryRepository + resolvedCommit + now).slice(0, 12)}`;
|
|
39
|
+
|
|
40
|
+
const snapshot = createResource('AgentMemorySnapshot', { name: snapshotName, namespace }, {
|
|
41
|
+
organizationRef,
|
|
42
|
+
memoryRepository,
|
|
43
|
+
requestedRef,
|
|
44
|
+
resolvedCommit,
|
|
45
|
+
queryManifestDigest,
|
|
46
|
+
selectedRecordsDigest,
|
|
47
|
+
selectedDocumentsDigest,
|
|
48
|
+
ontologyDigest: ontologyDigest || '',
|
|
49
|
+
recordCount: (selectedRecords || []).length,
|
|
50
|
+
documentCount: (selectedDocuments || []).length,
|
|
51
|
+
});
|
|
52
|
+
snapshot.status = { phase: 'Pinned', createdAt: now };
|
|
53
|
+
|
|
54
|
+
return snapshot;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
queryMemory({ snapshotRef, requester, query, records = [], documents = [], namespace = 'default', organizationRef = 'default' }) {
|
|
58
|
+
const modes = query.modes || ['graph-and-grep'];
|
|
59
|
+
const results = { graph: null, grep: null };
|
|
60
|
+
|
|
61
|
+
if (modes.includes('graph-only') || modes.includes('graph-and-grep')) {
|
|
62
|
+
results.graph = this.searchGraph({
|
|
63
|
+
records,
|
|
64
|
+
kinds: query.graph?.kinds || [],
|
|
65
|
+
edgeDepth: query.graph?.edgeDepth ?? 2,
|
|
66
|
+
query: query.text || '',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (modes.includes('grep-only') || modes.includes('graph-and-grep')) {
|
|
71
|
+
results.grep = this.searchGrep({
|
|
72
|
+
documents,
|
|
73
|
+
paths: query.grep?.paths || [],
|
|
74
|
+
pattern: query.text || '',
|
|
75
|
+
maxMatches: query.grep?.maxMatches ?? 25,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const queryName = `memquery-${sha256(snapshotRef + requester + now).slice(0, 12)}`;
|
|
81
|
+
|
|
82
|
+
const queryResource = createResource('AgentMemoryQuery', { name: queryName, namespace }, {
|
|
83
|
+
organizationRef,
|
|
84
|
+
snapshotRef,
|
|
85
|
+
requester,
|
|
86
|
+
query: clone(query),
|
|
87
|
+
resultDigest: sha256(JSON.stringify(results)),
|
|
88
|
+
});
|
|
89
|
+
queryResource.status = { phase: 'Completed', executedAt: now };
|
|
90
|
+
|
|
91
|
+
return { queryResource, results };
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Execute a memory search using the real query engine (agent-memory-query.js).
|
|
96
|
+
*
|
|
97
|
+
* @param {object} params
|
|
98
|
+
* @param {string} params.query - Search text (required, non-empty).
|
|
99
|
+
* @param {string} [params.mode] - 'graph-only' | 'grep-only' | 'graph-and-grep' (default 'graph-and-grep').
|
|
100
|
+
* @param {Array} [params.records] - Graph records.
|
|
101
|
+
* @param {Array} [params.edges] - Flat edge list.
|
|
102
|
+
* @param {Array} [params.documents] - Grep documents.
|
|
103
|
+
* @param {object} [params.graphOptions] - Passed to queryGraph: { kinds, depth }.
|
|
104
|
+
* @param {object} [params.grepOptions] - Passed to queryGrep: { paths, context, maxMatches }.
|
|
105
|
+
* @returns {{ graph: object|null, grep: object|null, stats: object }}
|
|
106
|
+
*/
|
|
107
|
+
queryAgentMemory({ query, mode = 'graph-and-grep', records = [], edges = [], documents = [], graphOptions = {}, grepOptions = {} }) {
|
|
108
|
+
return queryMemory({ query, mode, records, edges, documents, graphOptions, grepOptions });
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
searchGraph({ records, edges = [], kinds = [], edgeDepth = 2, query = '' }) {
|
|
112
|
+
// Empty query: return all records matching kinds filter with score=1 (no text match)
|
|
113
|
+
if (!query || query.trim() === '') {
|
|
114
|
+
let candidates = records;
|
|
115
|
+
if (kinds.length > 0) {
|
|
116
|
+
candidates = candidates.filter(r => kinds.includes(r.nodeKind));
|
|
117
|
+
}
|
|
118
|
+
const matches = candidates.map(record => ({
|
|
119
|
+
record: clone(record),
|
|
120
|
+
score: 1,
|
|
121
|
+
edges: this._collectEdges(record, records, edgeDepth),
|
|
122
|
+
}));
|
|
123
|
+
return { matches, totalMatches: matches.length };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Non-empty query: delegate to the real query engine
|
|
127
|
+
return queryGraph({
|
|
128
|
+
records,
|
|
129
|
+
edges,
|
|
130
|
+
query,
|
|
131
|
+
kinds,
|
|
132
|
+
depth: edgeDepth,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
_collectEdges(startRecord, allRecords, maxDepth) {
|
|
137
|
+
if (maxDepth <= 0 || !startRecord.edges || startRecord.edges.length === 0) return [];
|
|
138
|
+
|
|
139
|
+
const visited = new Set([startRecord.id]);
|
|
140
|
+
const collectedEdges = [];
|
|
141
|
+
let frontier = [startRecord];
|
|
142
|
+
|
|
143
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
144
|
+
const nextFrontier = [];
|
|
145
|
+
for (const record of frontier) {
|
|
146
|
+
for (const edge of (record.edges || [])) {
|
|
147
|
+
if (visited.has(edge.target)) continue;
|
|
148
|
+
visited.add(edge.target);
|
|
149
|
+
collectedEdges.push(clone(edge));
|
|
150
|
+
const targetRecord = allRecords.find(r => r.id === edge.target);
|
|
151
|
+
if (targetRecord) nextFrontier.push(targetRecord);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
frontier = nextFrontier;
|
|
155
|
+
if (frontier.length === 0) break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return collectedEdges;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
searchGrep({ documents, paths = [], pattern = '', maxMatches = 25 }) {
|
|
162
|
+
// Empty pattern: return empty (no-op, preserves backward-compatible behavior)
|
|
163
|
+
if (!pattern || pattern.trim() === '') {
|
|
164
|
+
return { excerpts: [], totalMatches: 0 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Delegate to the real query engine, mapping pattern -> query
|
|
168
|
+
return queryGrep({
|
|
169
|
+
documents,
|
|
170
|
+
query: pattern,
|
|
171
|
+
paths,
|
|
172
|
+
maxMatches,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
resolveTimeTravel({ mode = 'current', requestedRef, requestedTime, commits = [] }) {
|
|
177
|
+
const now = new Date().toISOString();
|
|
178
|
+
|
|
179
|
+
if (mode === 'current') {
|
|
180
|
+
const latest = commits[0] || null;
|
|
181
|
+
return {
|
|
182
|
+
resolvedCommit: latest?.sha || latest?.id || null,
|
|
183
|
+
resolvedAt: now,
|
|
184
|
+
mode: 'current',
|
|
185
|
+
staleBy: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (mode === 'explicit-ref') {
|
|
190
|
+
return {
|
|
191
|
+
resolvedCommit: requestedRef,
|
|
192
|
+
resolvedAt: now,
|
|
193
|
+
mode: 'explicit-ref',
|
|
194
|
+
staleBy: null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (mode === 'ref-at-time') {
|
|
199
|
+
const targetTime = new Date(requestedTime).getTime();
|
|
200
|
+
let best = null;
|
|
201
|
+
for (const commit of commits) {
|
|
202
|
+
const commitTime = new Date(commit.timestamp || commit.date).getTime();
|
|
203
|
+
if (commitTime <= targetTime) {
|
|
204
|
+
if (!best || commitTime > new Date(best.timestamp || best.date).getTime()) {
|
|
205
|
+
best = commit;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const resolvedCommit = best?.sha || best?.id || null;
|
|
210
|
+
const staleBy = best ? (targetTime - new Date(best.timestamp || best.date).getTime()) : null;
|
|
211
|
+
return {
|
|
212
|
+
resolvedCommit,
|
|
213
|
+
resolvedAt: now,
|
|
214
|
+
mode: 'ref-at-time',
|
|
215
|
+
staleBy,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (mode === 'snapshot-tag') {
|
|
220
|
+
return {
|
|
221
|
+
resolvedCommit: requestedRef,
|
|
222
|
+
resolvedAt: now,
|
|
223
|
+
mode: 'snapshot-tag',
|
|
224
|
+
staleBy: null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { resolvedCommit: null, resolvedAt: now, mode, staleBy: null };
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
createImport({ organizationRef, memoryRepository, source, include, validationPolicy, namespace = 'default' }) {
|
|
232
|
+
const now = new Date().toISOString();
|
|
233
|
+
const importName = `memimport-${sha256(memoryRepository + source + now).slice(0, 12)}`;
|
|
234
|
+
|
|
235
|
+
const importResource = createResource('AgentRunMemoryImport', { name: importName, namespace }, {
|
|
236
|
+
organizationRef,
|
|
237
|
+
memoryRepository,
|
|
238
|
+
source,
|
|
239
|
+
include: clone(include),
|
|
240
|
+
validationPolicy: validationPolicy || 'none',
|
|
241
|
+
});
|
|
242
|
+
importResource.status = { phase: 'Pending', createdAt: now };
|
|
243
|
+
|
|
244
|
+
return importResource;
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
processImport({ importResource, content }) {
|
|
248
|
+
const updated = clone(importResource);
|
|
249
|
+
const currentPhase = updated.status?.phase || 'Pending';
|
|
250
|
+
const now = new Date().toISOString();
|
|
251
|
+
|
|
252
|
+
const currentIndex = IMPORT_PHASES.indexOf(currentPhase);
|
|
253
|
+
if (currentIndex < 0 || currentIndex >= IMPORT_PHASES.length - 1) {
|
|
254
|
+
return updated;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const nextPhase = IMPORT_PHASES[currentIndex + 1];
|
|
258
|
+
|
|
259
|
+
// Apply phase-specific logic
|
|
260
|
+
if (nextPhase === 'Redacting') {
|
|
261
|
+
const scan = this.scanForRedaction(content || '');
|
|
262
|
+
updated.status.redactionScan = {
|
|
263
|
+
clean: scan.clean,
|
|
264
|
+
redactionCount: scan.redactionCount,
|
|
265
|
+
redactionsByKind: clone(scan.redactionsByKind),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
updated.status.phase = nextPhase;
|
|
270
|
+
updated.status.lastTransitionAt = now;
|
|
271
|
+
|
|
272
|
+
return updated;
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
scanForRedaction(content) {
|
|
276
|
+
if (typeof content !== 'string' || content.length === 0) {
|
|
277
|
+
return { clean: true, redactedContent: content || '', redactionCount: 0, redactionsByKind: {} };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let redacted = content;
|
|
281
|
+
const redactionsByKind = {};
|
|
282
|
+
let redactionCount = 0;
|
|
283
|
+
|
|
284
|
+
for (const { kind, pattern } of REDACTION_PATTERNS) {
|
|
285
|
+
const fresh = new RegExp(pattern.source, pattern.flags);
|
|
286
|
+
redacted = redacted.replace(fresh, (match) => {
|
|
287
|
+
redactionsByKind[kind] = (redactionsByKind[kind] || 0) + 1;
|
|
288
|
+
redactionCount++;
|
|
289
|
+
return `[REDACTED:${kind}]`;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
clean: redactionCount === 0,
|
|
295
|
+
redactedContent: redacted,
|
|
296
|
+
redactionCount,
|
|
297
|
+
redactionsByKind,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
validateOntology({ records, ontology }) {
|
|
302
|
+
const errors = [];
|
|
303
|
+
const requiredFields = ontology.requiredFields || {};
|
|
304
|
+
const allowedEdgeKinds = ontology.allowedEdgeKinds || [];
|
|
305
|
+
|
|
306
|
+
for (const record of records) {
|
|
307
|
+
const kind = record.nodeKind;
|
|
308
|
+
const fields = requiredFields[kind] || [];
|
|
309
|
+
|
|
310
|
+
for (const field of fields) {
|
|
311
|
+
const value = record.attributes?.[field];
|
|
312
|
+
if (value === undefined || value === null || value === '') {
|
|
313
|
+
errors.push({
|
|
314
|
+
record: record.id,
|
|
315
|
+
field,
|
|
316
|
+
message: `Missing required field '${field}' for nodeKind '${kind}'`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check edge kinds
|
|
322
|
+
for (const edge of (record.edges || [])) {
|
|
323
|
+
if (allowedEdgeKinds.length > 0 && !allowedEdgeKinds.includes(edge.kind)) {
|
|
324
|
+
errors.push({
|
|
325
|
+
record: record.id,
|
|
326
|
+
field: `edge.kind`,
|
|
327
|
+
message: `Edge kind '${edge.kind}' is not in allowedEdgeKinds`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { valid: errors.length === 0, errors };
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
createMemoryUpdate({ memoryRepository, sourceRun, changes, namespace = 'default', organizationRef = 'default' }) {
|
|
337
|
+
const now = new Date().toISOString();
|
|
338
|
+
const updateName = `memupdate-${sha256(memoryRepository + sourceRun + now).slice(0, 12)}`;
|
|
339
|
+
|
|
340
|
+
const update = createResource('AgentMemoryUpdate', { name: updateName, namespace }, {
|
|
341
|
+
organizationRef,
|
|
342
|
+
memoryRepository,
|
|
343
|
+
sourceRun,
|
|
344
|
+
changes: clone(changes),
|
|
345
|
+
});
|
|
346
|
+
update.status = { phase: 'Pending', createdAt: now };
|
|
347
|
+
|
|
348
|
+
return update;
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function globMatch(pattern, path) {
|
|
354
|
+
// Simple glob: convert * to regex .*
|
|
355
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
356
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
357
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Memory Import & Snapshot — Slice 2.3c
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - parseJournalForImport: parse a babysitter .a5c journal into a summary-only import payload
|
|
6
|
+
* - validateMemoryImport: validate an AgentRunMemoryImport spec
|
|
7
|
+
* - createMemorySnapshot: create a dispatch-time memory snapshot pinning record refs
|
|
8
|
+
* - validateMemorySnapshot: validate an AgentMemorySnapshot
|
|
9
|
+
* - validateOntology: validate an AgentMemoryOntology spec
|
|
10
|
+
* - getOntologyNodeKinds: return nodeKinds array from an ontology spec
|
|
11
|
+
* - getOntologyEdgeKinds: return edgeKinds array from an ontology spec
|
|
12
|
+
*
|
|
13
|
+
* Boundary: agent-memory-import
|
|
14
|
+
* - owns: journal parsing, summary extraction, snapshot creation, ontology validation
|
|
15
|
+
* - delegatesTo: (none — pure data transformation)
|
|
16
|
+
* - mustNotOwn: persistence, HTTP routing, Kubernetes resources, secret handling, git operations
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Journal parsing
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a babysitter .a5c journal array into a summary-only import payload.
|
|
27
|
+
* Returns structural metadata only — no raw task content, no arbitrary effect payloads.
|
|
28
|
+
*
|
|
29
|
+
* @param {Array} journal - Array of journal event objects
|
|
30
|
+
* @returns {{ summary: object, keyEvents: Array, effectSummary: object }}
|
|
31
|
+
*/
|
|
32
|
+
export function parseJournalForImport(journal) {
|
|
33
|
+
if (!Array.isArray(journal) || journal.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
summary: {
|
|
36
|
+
runId: null,
|
|
37
|
+
processId: null,
|
|
38
|
+
eventCount: 0,
|
|
39
|
+
durationMs: 0,
|
|
40
|
+
runStatus: null,
|
|
41
|
+
},
|
|
42
|
+
keyEvents: [],
|
|
43
|
+
effectSummary: {
|
|
44
|
+
successCount: 0,
|
|
45
|
+
failureCount: 0,
|
|
46
|
+
effectKinds: [],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Extract run metadata from run_start event
|
|
52
|
+
const runStart = journal.find(e => e.type === 'run_start');
|
|
53
|
+
const runEnd = journal.find(e => e.type === 'run_end');
|
|
54
|
+
|
|
55
|
+
const runId = runStart?.runId ?? null;
|
|
56
|
+
const processId = runStart?.processId ?? null;
|
|
57
|
+
const runStatus = runEnd?.status ?? null;
|
|
58
|
+
|
|
59
|
+
// Compute duration from timestamps
|
|
60
|
+
let durationMs = 0;
|
|
61
|
+
if (runStart?.timestamp && runEnd?.timestamp) {
|
|
62
|
+
const startMs = Date.parse(runStart.timestamp);
|
|
63
|
+
const endMs = Date.parse(runEnd.timestamp);
|
|
64
|
+
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
65
|
+
durationMs = endMs - startMs;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract key events (structural, no raw content)
|
|
70
|
+
const keyEvents = [];
|
|
71
|
+
let successCount = 0;
|
|
72
|
+
let failureCount = 0;
|
|
73
|
+
const effectKindSet = new Set();
|
|
74
|
+
|
|
75
|
+
for (const event of journal) {
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case 'run_start':
|
|
78
|
+
keyEvents.push({
|
|
79
|
+
type: 'run_start',
|
|
80
|
+
runId: event.runId ?? null,
|
|
81
|
+
processId: event.processId ?? null,
|
|
82
|
+
timestamp: event.timestamp ?? null,
|
|
83
|
+
});
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case 'task_completed': {
|
|
87
|
+
const keyEvent = {
|
|
88
|
+
type: 'task_completed',
|
|
89
|
+
taskId: event.taskId ?? null,
|
|
90
|
+
title: event.title ?? null,
|
|
91
|
+
status: event.status ?? null,
|
|
92
|
+
timestamp: event.timestamp ?? null,
|
|
93
|
+
};
|
|
94
|
+
// Strip raw effect to summary-only (kind + result only)
|
|
95
|
+
if (event.effect && typeof event.effect === 'object') {
|
|
96
|
+
keyEvent.effect = {
|
|
97
|
+
kind: event.effect.kind ?? null,
|
|
98
|
+
result: event.effect.result ?? null,
|
|
99
|
+
};
|
|
100
|
+
if (event.effect.kind) effectKindSet.add(event.effect.kind);
|
|
101
|
+
if (event.effect.result === 'success') successCount++;
|
|
102
|
+
else if (event.effect.result === 'failure') failureCount++;
|
|
103
|
+
} else if (event.status === 'success') {
|
|
104
|
+
successCount++;
|
|
105
|
+
} else if (event.status === 'failure') {
|
|
106
|
+
failureCount++;
|
|
107
|
+
}
|
|
108
|
+
keyEvents.push(keyEvent);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'breakpoint':
|
|
113
|
+
keyEvents.push({
|
|
114
|
+
type: 'breakpoint',
|
|
115
|
+
reason: event.reason ?? null,
|
|
116
|
+
timestamp: event.timestamp ?? null,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'run_end':
|
|
121
|
+
keyEvents.push({
|
|
122
|
+
type: 'run_end',
|
|
123
|
+
status: event.status ?? null,
|
|
124
|
+
timestamp: event.timestamp ?? null,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
// Include other event types with only structural metadata
|
|
130
|
+
keyEvents.push({
|
|
131
|
+
type: event.type,
|
|
132
|
+
timestamp: event.timestamp ?? null,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
summary: {
|
|
140
|
+
runId,
|
|
141
|
+
processId,
|
|
142
|
+
eventCount: journal.length,
|
|
143
|
+
durationMs,
|
|
144
|
+
runStatus,
|
|
145
|
+
},
|
|
146
|
+
keyEvents,
|
|
147
|
+
effectSummary: {
|
|
148
|
+
successCount,
|
|
149
|
+
failureCount,
|
|
150
|
+
effectKinds: Array.from(effectKindSet),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// validateMemoryImport
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate an AgentRunMemoryImport spec.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} importSpec
|
|
163
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
164
|
+
*/
|
|
165
|
+
export function validateMemoryImport(importSpec) {
|
|
166
|
+
const errors = [];
|
|
167
|
+
|
|
168
|
+
if (!importSpec || typeof importSpec !== 'object') {
|
|
169
|
+
return { valid: false, errors: ['importSpec must be a non-null object'] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!importSpec.name || typeof importSpec.name !== 'string' || importSpec.name.trim() === '') {
|
|
173
|
+
errors.push('name is required and must be a non-empty string');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!importSpec.organizationRef || typeof importSpec.organizationRef !== 'string' || importSpec.organizationRef.trim() === '') {
|
|
177
|
+
errors.push('organizationRef is required and must be a non-empty string');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!importSpec.runId || typeof importSpec.runId !== 'string' || importSpec.runId.trim() === '') {
|
|
181
|
+
errors.push('runId is required and must be a non-empty string');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { valid: errors.length === 0, errors };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// createMemorySnapshot
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a dispatch-time memory snapshot pinning record refs.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} params
|
|
195
|
+
* @param {string} params.sessionRef - The agent session reference (required)
|
|
196
|
+
* @param {string} params.organizationRef - Organization reference
|
|
197
|
+
* @param {Array} params.recordRefs - Array of record reference strings
|
|
198
|
+
* @param {object} [params.queryCriteria] - Optional query criteria used to select records
|
|
199
|
+
* @returns {object} snapshot
|
|
200
|
+
*/
|
|
201
|
+
export function createMemorySnapshot({ sessionRef, organizationRef, recordRefs = [], queryCriteria }) {
|
|
202
|
+
const snapshotId = `snap-${randomBytes(8).toString('hex')}`;
|
|
203
|
+
const createdAt = new Date().toISOString();
|
|
204
|
+
|
|
205
|
+
const snapshot = {
|
|
206
|
+
snapshotId,
|
|
207
|
+
sessionRef: sessionRef ?? null,
|
|
208
|
+
organizationRef: organizationRef ?? null,
|
|
209
|
+
recordRefs: Array.isArray(recordRefs) ? [...recordRefs] : [],
|
|
210
|
+
createdAt,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (queryCriteria !== undefined && queryCriteria !== null) {
|
|
214
|
+
snapshot.queryCriteria = { ...queryCriteria };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return snapshot;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// validateMemorySnapshot
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Validate an AgentMemorySnapshot object.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} snapshot
|
|
228
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
229
|
+
*/
|
|
230
|
+
export function validateMemorySnapshot(snapshot) {
|
|
231
|
+
const errors = [];
|
|
232
|
+
|
|
233
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
234
|
+
return { valid: false, errors: ['snapshot must be a non-null object'] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!snapshot.snapshotId || typeof snapshot.snapshotId !== 'string' || snapshot.snapshotId.trim() === '') {
|
|
238
|
+
errors.push('snapshotId is required and must be a non-empty string');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!snapshot.sessionRef || typeof snapshot.sessionRef !== 'string' || snapshot.sessionRef.trim() === '') {
|
|
242
|
+
errors.push('sessionRef is required and must be a non-empty string');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { valid: errors.length === 0, errors };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// validateOntology (standalone)
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Validate an AgentMemoryOntology spec.
|
|
254
|
+
*
|
|
255
|
+
* @param {object} ontologySpec
|
|
256
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
257
|
+
*/
|
|
258
|
+
export function validateOntology(ontologySpec) {
|
|
259
|
+
const errors = [];
|
|
260
|
+
|
|
261
|
+
if (!ontologySpec || typeof ontologySpec !== 'object') {
|
|
262
|
+
return { valid: false, errors: ['ontologySpec must be a non-null object'] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!ontologySpec.name || typeof ontologySpec.name !== 'string' || ontologySpec.name.trim() === '') {
|
|
266
|
+
errors.push('name is required and must be a non-empty string');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!ontologySpec.organizationRef || typeof ontologySpec.organizationRef !== 'string' || ontologySpec.organizationRef.trim() === '') {
|
|
270
|
+
errors.push('organizationRef is required and must be a non-empty string');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// nodeKinds must be a non-empty array
|
|
274
|
+
const nodeKinds = ontologySpec.nodeKinds;
|
|
275
|
+
if (!Array.isArray(nodeKinds) || nodeKinds.length === 0) {
|
|
276
|
+
errors.push('nodeKinds must be a non-empty array');
|
|
277
|
+
} else {
|
|
278
|
+
// Check for duplicate nodeKind names
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
for (const nk of nodeKinds) {
|
|
281
|
+
const kindName = nk?.name;
|
|
282
|
+
if (kindName !== undefined && kindName !== null) {
|
|
283
|
+
if (seen.has(kindName)) {
|
|
284
|
+
errors.push(`Duplicate nodeKind name: "${kindName}"`);
|
|
285
|
+
} else {
|
|
286
|
+
seen.add(kindName);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { valid: errors.length === 0, errors };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// getOntologyNodeKinds
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Return the nodeKinds array from an ontology spec.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} ontologySpec
|
|
303
|
+
* @returns {Array}
|
|
304
|
+
*/
|
|
305
|
+
export function getOntologyNodeKinds(ontologySpec) {
|
|
306
|
+
if (!ontologySpec || !Array.isArray(ontologySpec.nodeKinds)) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
return [...ontologySpec.nodeKinds];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// getOntologyEdgeKinds
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Return the edgeKinds array from an ontology spec.
|
|
318
|
+
*
|
|
319
|
+
* @param {object} ontologySpec
|
|
320
|
+
* @returns {Array}
|
|
321
|
+
*/
|
|
322
|
+
export function getOntologyEdgeKinds(ontologySpec) {
|
|
323
|
+
if (!ontologySpec || !Array.isArray(ontologySpec.edgeKinds)) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
return [...ontologySpec.edgeKinds];
|
|
327
|
+
}
|