@feltdb/core 0.2.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.
- package/README.md +184 -0
- package/dist/agent-decision.d.ts +81 -0
- package/dist/agent-decision.d.ts.map +1 -0
- package/dist/agent-decision.js +18 -0
- package/dist/agent-memory.d.ts +91 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +21 -0
- package/dist/agent-observation.d.ts +61 -0
- package/dist/agent-observation.d.ts.map +1 -0
- package/dist/agent-observation.js +12 -0
- package/dist/agent-registry.d.ts +92 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +134 -0
- package/dist/agent-runtime.d.ts +147 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +240 -0
- package/dist/agent.d.ts +132 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +58 -0
- package/dist/capability.d.ts +21 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +23 -0
- package/dist/collection.d.ts +95 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +289 -0
- package/dist/db.d.ts +504 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +700 -0
- package/dist/execution.d.ts +148 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +18 -0
- package/dist/feltdb.d.ts +82 -0
- package/dist/feltdb.d.ts.map +1 -0
- package/dist/feltdb.js +6 -0
- package/dist/flowspec.d.ts +64 -0
- package/dist/flowspec.d.ts.map +1 -0
- package/dist/flowspec.js +272 -0
- package/dist/http-db.d.ts +50 -0
- package/dist/http-db.d.ts.map +1 -0
- package/dist/http-db.js +205 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/indexeddb-db.d.ts +54 -0
- package/dist/indexeddb-db.d.ts.map +1 -0
- package/dist/indexeddb-db.js +175 -0
- package/dist/memory-db.d.ts +49 -0
- package/dist/memory-db.d.ts.map +1 -0
- package/dist/memory-db.js +97 -0
- package/dist/operation.d.ts +25 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +16 -0
- package/dist/reactive-graph.d.ts +67 -0
- package/dist/reactive-graph.d.ts.map +1 -0
- package/dist/reactive-graph.js +118 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +104 -0
- package/dist/storage.d.ts +48 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +6 -0
- package/dist/workflow.d.ts +20 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +12 -0
- package/package.json +43 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB State-First Database API
|
|
3
|
+
*
|
|
4
|
+
* High-level, application-facing API that treats FeltDB as application state.
|
|
5
|
+
* Persistence, indexing, and reactivity are completely transparent.
|
|
6
|
+
*/
|
|
7
|
+
import { Collection, Relationship } from './collection.js';
|
|
8
|
+
import { createAgentRef } from './agent.js';
|
|
9
|
+
import { AgentRegistry } from './agent-registry.js';
|
|
10
|
+
import { AgentRuntime } from './agent-runtime.js';
|
|
11
|
+
import { MemoryJsDb } from './memory-db.js';
|
|
12
|
+
import { HttpJsDb } from './http-db.js';
|
|
13
|
+
import { IndexedDbJsDb } from './indexeddb-db.js';
|
|
14
|
+
import { emptyFlowSpec, planFlowSpecMigration, validateFlowSpec } from './flowspec.js';
|
|
15
|
+
export function createFeltDB(options) {
|
|
16
|
+
if (!options?.namespace?.trim()) {
|
|
17
|
+
throw new Error('createFeltDB requires a non-empty namespace');
|
|
18
|
+
}
|
|
19
|
+
const runtime = options.server ? new HttpJsDb(options.server)
|
|
20
|
+
: 'browser' in options && options.browser ? new IndexedDbJsDb(options.namespace)
|
|
21
|
+
: new MemoryJsDb(options.namespace);
|
|
22
|
+
return new StateFirstDB(runtime);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* State-first database interface.
|
|
26
|
+
* Applications use this to treat FeltDB collections as live application state.
|
|
27
|
+
*/
|
|
28
|
+
export class StateFirstDB {
|
|
29
|
+
constructor(jsDb) {
|
|
30
|
+
this.collections = new Map();
|
|
31
|
+
this.capabilityWorkers = new Map();
|
|
32
|
+
this.jsDb = jsDb;
|
|
33
|
+
this.runtimeInfo = this.detectRuntime();
|
|
34
|
+
this.agentRegistry = new AgentRegistry();
|
|
35
|
+
const agentConfig = {
|
|
36
|
+
persistent: this.runtimeInfo.persistent,
|
|
37
|
+
executionTimeoutMs: 30000,
|
|
38
|
+
supportsReactiveTriggers: true,
|
|
39
|
+
defaultRetryPolicy: {
|
|
40
|
+
maxAttempts: 3,
|
|
41
|
+
backoffMs: 1000,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
this.agentRuntime = new AgentRuntime(this.agentRegistry, agentConfig);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Detect runtime environment and storage characteristics
|
|
48
|
+
*/
|
|
49
|
+
detectRuntime() {
|
|
50
|
+
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
51
|
+
const isNode = typeof globalThis !== 'undefined' && 'versions' in globalThis && 'node' in globalThis.versions;
|
|
52
|
+
const isMemory = this.jsDb instanceof MemoryJsDb;
|
|
53
|
+
const isRemote = this.jsDb instanceof HttpJsDb;
|
|
54
|
+
const isIndexedDb = this.jsDb instanceof IndexedDbJsDb;
|
|
55
|
+
// Detect storage backend
|
|
56
|
+
let storage = 'memory';
|
|
57
|
+
let persistent = false;
|
|
58
|
+
if (isRemote) {
|
|
59
|
+
storage = 'remote';
|
|
60
|
+
persistent = true;
|
|
61
|
+
}
|
|
62
|
+
else if (isMemory) {
|
|
63
|
+
storage = 'memory';
|
|
64
|
+
}
|
|
65
|
+
else if (isIndexedDb) {
|
|
66
|
+
storage = 'indexeddb';
|
|
67
|
+
persistent = true;
|
|
68
|
+
}
|
|
69
|
+
else if (isNode) {
|
|
70
|
+
storage = 'file';
|
|
71
|
+
persistent = true;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
runtime: isRemote ? 'remote' : isIndexedDb || isBrowser ? 'browser' : isNode ? 'node' : 'wasm',
|
|
75
|
+
storage,
|
|
76
|
+
persistent,
|
|
77
|
+
reactive: true, // FeltDB is always reactive
|
|
78
|
+
durable: persistent,
|
|
79
|
+
version: '0.1.0',
|
|
80
|
+
supportsCheckpointing: persistent,
|
|
81
|
+
supportsLifecycle: isBrowser || isIndexedDb,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get runtime information about this FeltDB instance.
|
|
86
|
+
*
|
|
87
|
+
* Useful for testing and debugging without contaminating the normal API.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* const runtime = db.runtime();
|
|
91
|
+
* console.log(`Using ${runtime.storage} storage in ${runtime.runtime}`);
|
|
92
|
+
*/
|
|
93
|
+
runtime() {
|
|
94
|
+
return { ...this.runtimeInfo };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get or create a collection.
|
|
98
|
+
*
|
|
99
|
+
* Collections are live application state - they automatically update
|
|
100
|
+
* whenever underlying data changes. No manual refresh() or invalidate() needed.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* const tasks = db.collection("tasks");
|
|
104
|
+
* const task = await tasks.get(123);
|
|
105
|
+
* const allActive = await tasks.where(t => t.status === "active").all();
|
|
106
|
+
*/
|
|
107
|
+
collection(name) {
|
|
108
|
+
if (!this.collections.has(name)) {
|
|
109
|
+
this.collections.set(name, new Collection(this.jsDb, name));
|
|
110
|
+
}
|
|
111
|
+
return this.collections.get(name);
|
|
112
|
+
}
|
|
113
|
+
/** Resolve local state, causally acquiring it from configured peers when absent. */
|
|
114
|
+
async acquire(collection, id) {
|
|
115
|
+
if (this.jsDb instanceof HttpJsDb)
|
|
116
|
+
return this.jsDb.acquire(collection, id);
|
|
117
|
+
return this.collection(collection).get(id);
|
|
118
|
+
}
|
|
119
|
+
/** Search canonical collection state locally or through the remote capability surface. */
|
|
120
|
+
async search(collection, query, limit = 50) {
|
|
121
|
+
if (this.jsDb instanceof HttpJsDb)
|
|
122
|
+
return this.jsDb.search(collection, query, limit);
|
|
123
|
+
const needle = query.toLowerCase();
|
|
124
|
+
return (await this.collection(collection).all())
|
|
125
|
+
.filter(value => JSON.stringify(value).toLowerCase().includes(needle)).slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
/** Install a resource-bounded declarative capability program. */
|
|
128
|
+
async defineCapability(name, steps) {
|
|
129
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
130
|
+
throw new Error('Distributed capabilities require a server runtime');
|
|
131
|
+
return this.jsDb.command(`/capabilities/${encodeURIComponent(name)}`, { steps });
|
|
132
|
+
}
|
|
133
|
+
/** Attach executable behavior to a capability in an embedded runtime. */
|
|
134
|
+
registerCapabilityWorker(name, handler) {
|
|
135
|
+
if (this.jsDb instanceof HttpJsDb)
|
|
136
|
+
throw new Error('Remote capability workers are installed on the server');
|
|
137
|
+
this.capabilityWorkers.set(name, handler);
|
|
138
|
+
return () => this.capabilityWorkers.delete(name);
|
|
139
|
+
}
|
|
140
|
+
async executeCapability(name, input) {
|
|
141
|
+
if (this.jsDb instanceof HttpJsDb)
|
|
142
|
+
return this.jsDb.command(`/capabilities/${encodeURIComponent(name)}/execute`, input);
|
|
143
|
+
const worker = this.capabilityWorkers.get(name);
|
|
144
|
+
if (!worker)
|
|
145
|
+
throw new Error(`No embedded capability worker registered for ${name}`);
|
|
146
|
+
const id = `cap-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
147
|
+
const executions = this.collection('_flow_executions');
|
|
148
|
+
await executions.insert({ id, capability: name, input, status: 'running', owner: this.instanceId(), started_at: Date.now() }, id);
|
|
149
|
+
try {
|
|
150
|
+
const output = await worker(input);
|
|
151
|
+
await executions.update(id, { status: 'completed', output, completed_at: Date.now() });
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
await executions.update(id, { status: 'failed', error: error instanceof Error ? error.message : String(error), completed_at: Date.now() });
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Joint-consensus membership change. `members` contains every voting node, including this node. */
|
|
160
|
+
async changeClusterMembership(expectedEpoch, members) {
|
|
161
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
162
|
+
throw new Error('Cluster membership requires a server runtime');
|
|
163
|
+
return this.jsDb.command('/cluster/members', { expected_epoch: expectedEpoch, peers: members });
|
|
164
|
+
}
|
|
165
|
+
/** Version and deploy one canonical application model into FeltDB primitives. */
|
|
166
|
+
async deployFlowSpec(spec, expectedVersion, allowDestructive = false) {
|
|
167
|
+
const diagnostics = validateFlowSpec(spec).filter(value => value.severity === 'error');
|
|
168
|
+
if (diagnostics.length)
|
|
169
|
+
throw new Error(diagnostics.map(value => value.message).join('; '));
|
|
170
|
+
const applications = this.collection('_flow_apps');
|
|
171
|
+
const current = await applications.get(spec.app);
|
|
172
|
+
const version = (current?.version ?? 0) + 1;
|
|
173
|
+
if (expectedVersion !== undefined && (current?.version ?? 0) !== expectedVersion)
|
|
174
|
+
throw new Error(`FlowSpec version conflict: expected ${expectedVersion}, found ${current?.version ?? 0}`);
|
|
175
|
+
const migration = planFlowSpecMigration(current?.spec ?? emptyFlowSpec(spec.app), spec);
|
|
176
|
+
const destructive = migration.filter(operation => operation.safety === 'destructive');
|
|
177
|
+
if (destructive.length && !allowDestructive)
|
|
178
|
+
throw new Error(`Destructive migration requires explicit approval: ${destructive.map(operation => operation.target).join(', ')}`);
|
|
179
|
+
const deployment = { id: spec.app, app: spec.app, version, status: 'deploying', spec, migration, deployed_at: Date.now() };
|
|
180
|
+
if (current)
|
|
181
|
+
await applications.update(spec.app, deployment);
|
|
182
|
+
else
|
|
183
|
+
await applications.insert(deployment, spec.app);
|
|
184
|
+
const modelId = (kind, name) => `${spec.app}-${kind}-${name}`.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
185
|
+
const reconcile = async (collectionName, kind, currentValues, nextValues) => {
|
|
186
|
+
const collection = this.collection(collectionName);
|
|
187
|
+
const nextNames = new Set(nextValues.map(value => value.name ?? value.event ?? 'unnamed'));
|
|
188
|
+
for (const value of currentValues) {
|
|
189
|
+
const name = value.name ?? value.event ?? 'unnamed';
|
|
190
|
+
if (!nextNames.has(name))
|
|
191
|
+
await collection.delete(modelId(kind, name));
|
|
192
|
+
}
|
|
193
|
+
for (const value of nextValues) {
|
|
194
|
+
const name = value.name ?? value.event ?? 'unnamed';
|
|
195
|
+
const id = modelId(kind, name);
|
|
196
|
+
const record = { id, app: spec.app, version, ...value };
|
|
197
|
+
if (await collection.get(id))
|
|
198
|
+
await collection.update(id, record);
|
|
199
|
+
else
|
|
200
|
+
await collection.insert(record, id);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const previous = current?.spec ?? emptyFlowSpec(spec.app);
|
|
204
|
+
await reconcile('_flow_collection_models', 'collection', previous.collections, spec.collections);
|
|
205
|
+
await reconcile('_flow_capability_models', 'capability', previous.capabilities, spec.capabilities);
|
|
206
|
+
await reconcile('_flow_trigger_models', 'trigger', previous.triggers, spec.triggers);
|
|
207
|
+
await reconcile('_flow_policy_models', 'policy', previous.policies, spec.policies);
|
|
208
|
+
await reconcile('_flow_schedule_models', 'schedule', previous.schedules, spec.schedules);
|
|
209
|
+
if (this.jsDb instanceof HttpJsDb) {
|
|
210
|
+
for (const workflow of previous.workflows)
|
|
211
|
+
if (!spec.workflows.some(value => value.name === workflow.name))
|
|
212
|
+
await this.collection('_flow_workflows').delete(modelId('workflow', workflow.name));
|
|
213
|
+
for (const agent of previous.agents)
|
|
214
|
+
if (!spec.agents.some(value => value.name === agent.name))
|
|
215
|
+
await this.collection('_flow_agents').delete(modelId('agent', agent.name));
|
|
216
|
+
for (const workflow of spec.workflows)
|
|
217
|
+
await this.defineWorkflow(modelId('workflow', workflow.name), workflow.steps.map(step => step.name));
|
|
218
|
+
for (const agent of spec.agents) {
|
|
219
|
+
const capabilities = agent.statements.filter(line => line.startsWith('capability ')).map(line => line.slice('capability '.length).trim());
|
|
220
|
+
await this.defineStateAgent(modelId('agent', agent.name), capabilities, { flowspec_app: spec.app, flowspec_version: version });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Embedded runtimes materialize the same application model as ordinary
|
|
225
|
+
// durable state. Network workers can activate it later without a second
|
|
226
|
+
// schema or a Studio-only representation.
|
|
227
|
+
await reconcile('_flow_workflows', 'workflow', previous.workflows, spec.workflows);
|
|
228
|
+
await reconcile('_flow_agents', 'agent', previous.agents, spec.agents);
|
|
229
|
+
}
|
|
230
|
+
const active = { ...deployment, status: 'active' };
|
|
231
|
+
await applications.update(spec.app, active);
|
|
232
|
+
await this.collection('_flow_app_versions').insert({ id: `${spec.app}-${version}`, app: spec.app, version, spec, migration, deployed_at: active.deployed_at }, `${spec.app}-${version}`);
|
|
233
|
+
return { app: spec.app, version, status: 'active' };
|
|
234
|
+
}
|
|
235
|
+
/** Read the runtime's durable local mutation log for inspection and audit. */
|
|
236
|
+
async auditEvents() {
|
|
237
|
+
if (!this.jsDb.audit_events)
|
|
238
|
+
return [];
|
|
239
|
+
return await this.jsDb.audit_events();
|
|
240
|
+
}
|
|
241
|
+
/** Export durable embedded operations for transport over any application channel. */
|
|
242
|
+
async exportOperations(sinceSequence = 0) {
|
|
243
|
+
if (!this.jsDb.export_operations)
|
|
244
|
+
throw new Error('This runtime uses server-managed replication');
|
|
245
|
+
return await this.jsDb.export_operations(sinceSequence);
|
|
246
|
+
}
|
|
247
|
+
/** Merge operations from another embedded replica and notify live collections. */
|
|
248
|
+
async applyOperations(operations) {
|
|
249
|
+
if (!this.jsDb.apply_remote_operations)
|
|
250
|
+
throw new Error('This runtime uses server-managed replication');
|
|
251
|
+
return await this.jsDb.apply_remote_operations(operations);
|
|
252
|
+
}
|
|
253
|
+
/** Exchange durable operations with another embedded replica in both directions. */
|
|
254
|
+
async synchronizeWith(peer) {
|
|
255
|
+
const localOperations = await this.exportOperations();
|
|
256
|
+
const peerOperations = await peer.exportOperations();
|
|
257
|
+
await this.addSyncPeer(peer.instanceId());
|
|
258
|
+
await peer.addSyncPeer(this.instanceId());
|
|
259
|
+
const remoteResult = await peer.applyOperations(localOperations);
|
|
260
|
+
const localResult = await this.applyOperations(peerOperations);
|
|
261
|
+
return { sent: localOperations.length, received: peerOperations.length, applied: localResult.applied + remoteResult.applied, ignored: localResult.ignored + remoteResult.ignored };
|
|
262
|
+
}
|
|
263
|
+
/** Route a capability to the first available embedded provider and audit failover attempts. */
|
|
264
|
+
async executeCapabilityWithFailover(name, input, providers = []) {
|
|
265
|
+
const routes = this.collection('_flow_capability_routes');
|
|
266
|
+
const routeId = `route-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
267
|
+
const candidates = [this, ...providers];
|
|
268
|
+
const failures = [];
|
|
269
|
+
await routes.insert({ id: routeId, capability: name, status: 'routing', input, candidates: candidates.map(value => value.instanceId()), started_at: Date.now() }, routeId);
|
|
270
|
+
for (const provider of candidates) {
|
|
271
|
+
try {
|
|
272
|
+
const output = await provider.executeCapability(name, input);
|
|
273
|
+
await routes.update(routeId, { status: 'completed', provider: provider.instanceId(), attempts: failures.length + 1, failures, completed_at: Date.now() });
|
|
274
|
+
return { output, provider: provider.instanceId(), attempts: failures.length + 1 };
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
failures.push({ provider: provider.instanceId(), error: error instanceof Error ? error.message : String(error) });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
await routes.update(routeId, { status: 'failed', attempts: failures.length, failures, completed_at: Date.now() });
|
|
281
|
+
throw new Error(`No provider could execute capability ${name}: ${failures.map(value => value.error).join('; ')}`);
|
|
282
|
+
}
|
|
283
|
+
/** Inspect the causal operation that currently materializes a remote record. */
|
|
284
|
+
async recordProvenance(collection, id) {
|
|
285
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
286
|
+
throw new Error('Causal provenance requires a server runtime');
|
|
287
|
+
return this.jsDb.provenance(collection, id);
|
|
288
|
+
}
|
|
289
|
+
/** Persist immutable content by hash; repeated bytes deduplicate automatically. */
|
|
290
|
+
async storeContent(content) {
|
|
291
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
292
|
+
throw new Error('Durable content storage requires a server runtime');
|
|
293
|
+
return this.jsDb.storeContent(content);
|
|
294
|
+
}
|
|
295
|
+
/** Resolve verified immutable content locally or from configured peers. */
|
|
296
|
+
async acquireContent(hash) {
|
|
297
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
298
|
+
throw new Error('Network content acquisition requires a server runtime');
|
|
299
|
+
return this.jsDb.acquireContent(hash);
|
|
300
|
+
}
|
|
301
|
+
/** Define a durable workflow whose lifecycle is ordinary replicated state. */
|
|
302
|
+
async defineWorkflow(name, steps) {
|
|
303
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
304
|
+
throw new Error('Durable workflow coordination requires a server runtime');
|
|
305
|
+
return this.jsDb.command(`/workflows/${encodeURIComponent(name)}`, { steps });
|
|
306
|
+
}
|
|
307
|
+
async startWorkflow(name, input = null) {
|
|
308
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
309
|
+
throw new Error('Durable workflow coordination requires a server runtime');
|
|
310
|
+
return this.jsDb.command(`/workflows/${encodeURIComponent(name)}/runs`, { input });
|
|
311
|
+
}
|
|
312
|
+
async claimWorkflowStep(runId, step, worker, leaseMs = 30000) {
|
|
313
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
314
|
+
throw new Error('Durable workflow coordination requires a server runtime');
|
|
315
|
+
return this.jsDb.command(`/workflow-runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(step)}/claim`, { worker, lease_ms: leaseMs });
|
|
316
|
+
}
|
|
317
|
+
async completeWorkflowStep(runId, step, claimId, result = null) {
|
|
318
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
319
|
+
throw new Error('Durable workflow coordination requires a server runtime');
|
|
320
|
+
return this.jsDb.command(`/workflow-runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(step)}/complete`, { claim_id: claimId, result });
|
|
321
|
+
}
|
|
322
|
+
/** Define and start state-first agents; workers observe and advance these records externally. */
|
|
323
|
+
async defineStateAgent(name, capabilities = [], constraints = null) {
|
|
324
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
325
|
+
throw new Error('Durable agent coordination requires a server runtime');
|
|
326
|
+
return this.jsDb.command(`/agents/${encodeURIComponent(name)}`, { capabilities, constraints });
|
|
327
|
+
}
|
|
328
|
+
async startStateAgent(name, goal, input = null) {
|
|
329
|
+
if (!(this.jsDb instanceof HttpJsDb))
|
|
330
|
+
throw new Error('Durable agent coordination requires a server runtime');
|
|
331
|
+
return this.jsDb.command(`/agents/${encodeURIComponent(name)}/runs`, { goal, input });
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Define an agent with declarative configuration.
|
|
335
|
+
*
|
|
336
|
+
* Agents are durable, addressable participants in the FeltDB fabric.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* const researcher = db.defineAgent({
|
|
340
|
+
* name: "researcher",
|
|
341
|
+
* version: 1,
|
|
342
|
+
* capabilities: ["vector-search", "document-read", "report-write"],
|
|
343
|
+
* constraints: { maxLatency: 5000 }
|
|
344
|
+
* });
|
|
345
|
+
*/
|
|
346
|
+
defineAgent(definition) {
|
|
347
|
+
const agentRef = createAgentRef(definition.name, definition.version);
|
|
348
|
+
this.agentRegistry.register(agentRef, definition);
|
|
349
|
+
return agentRef;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get an agent by name.
|
|
353
|
+
*
|
|
354
|
+
* Returns the latest version of the agent if it exists.
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* const researcher = db.agent("researcher");
|
|
358
|
+
*/
|
|
359
|
+
agent(name) {
|
|
360
|
+
const entry = this.agentRegistry.getByName(name);
|
|
361
|
+
return entry?.agentRef;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get the agent registry
|
|
365
|
+
*/
|
|
366
|
+
getAgentRegistry() {
|
|
367
|
+
return this.agentRegistry;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Get the agent runtime
|
|
371
|
+
*/
|
|
372
|
+
getAgentRuntime() {
|
|
373
|
+
return this.agentRuntime;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Create an agent execution
|
|
377
|
+
*/
|
|
378
|
+
async createAgentExecution(agentRef, goal, inputs) {
|
|
379
|
+
const execution = await this.agentRuntime.createExecution(this, agentRef, goal, inputs);
|
|
380
|
+
await this.collection('_flow_agent_executions').insert(this.agentExecutionRecord(execution), execution.executionId);
|
|
381
|
+
return execution;
|
|
382
|
+
}
|
|
383
|
+
agentExecutionRecord(execution) {
|
|
384
|
+
return { ...execution, id: execution.executionId, agentRef: execution.agentRef.toString() };
|
|
385
|
+
}
|
|
386
|
+
/** Claim and durably materialize an embedded agent execution. */
|
|
387
|
+
async startAgentExecution(execution, peerId = this.instanceId()) {
|
|
388
|
+
await this.agentRuntime.start(execution, peerId);
|
|
389
|
+
await this.collection('_flow_agent_executions').update(execution.executionId, this.agentExecutionRecord(execution));
|
|
390
|
+
}
|
|
391
|
+
/** Advance an agent lifecycle and expose the transition as ordinary state. */
|
|
392
|
+
async transitionAgentExecution(execution, status) {
|
|
393
|
+
await this.agentRuntime.transition(execution, status);
|
|
394
|
+
await this.collection('_flow_agent_executions').update(execution.executionId, this.agentExecutionRecord(execution));
|
|
395
|
+
}
|
|
396
|
+
async completeAgentExecution(execution, resultRef) {
|
|
397
|
+
await this.agentRuntime.complete(execution, resultRef);
|
|
398
|
+
await this.collection('_flow_agent_executions').update(execution.executionId, this.agentExecutionRecord(execution));
|
|
399
|
+
}
|
|
400
|
+
async failAgentExecution(execution, error) {
|
|
401
|
+
await this.agentRuntime.fail(execution, error);
|
|
402
|
+
await this.collection('_flow_agent_executions').update(execution.executionId, this.agentExecutionRecord(execution));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Close the database and clean up resources.
|
|
406
|
+
*/
|
|
407
|
+
async close() {
|
|
408
|
+
for (const collection of this.collections.values()) {
|
|
409
|
+
collection.close();
|
|
410
|
+
}
|
|
411
|
+
this.collections.clear();
|
|
412
|
+
await this.jsDb.close?.();
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get current sync state information.
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* const syncState = db.sync();
|
|
419
|
+
* console.log(`Connected to ${syncState.connected_peers.length} peers`);
|
|
420
|
+
*/
|
|
421
|
+
sync() {
|
|
422
|
+
const result = this.jsDb.sync_info();
|
|
423
|
+
if (!result.success || !result.data) {
|
|
424
|
+
throw new Error(result.error || 'Failed to get sync info');
|
|
425
|
+
}
|
|
426
|
+
return JSON.parse(result.data);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Add a peer for synchronization.
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* await db.addSyncPeer('peer-123');
|
|
433
|
+
*/
|
|
434
|
+
async addSyncPeer(peerId) {
|
|
435
|
+
const result = this.jsDb.add_sync_peer(peerId);
|
|
436
|
+
if (!result.success) {
|
|
437
|
+
throw new Error(result.error || 'Failed to add peer');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Remove a peer from synchronization.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* await db.removeSyncPeer('peer-123');
|
|
445
|
+
*/
|
|
446
|
+
async removeSyncPeer(peerId) {
|
|
447
|
+
const result = this.jsDb.remove_sync_peer(peerId);
|
|
448
|
+
if (!result.success) {
|
|
449
|
+
throw new Error(result.error || 'Failed to remove peer');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get pending operations for a peer.
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* const ops = await db.getPendingForPeer('peer-123', 5);
|
|
457
|
+
*/
|
|
458
|
+
async getPendingForPeer(peerId, sinceSequence) {
|
|
459
|
+
const result = this.jsDb.get_pending_for_peer(peerId, sinceSequence);
|
|
460
|
+
if (!result.success || !result.data) {
|
|
461
|
+
throw new Error(result.error || 'Failed to get pending operations');
|
|
462
|
+
}
|
|
463
|
+
return JSON.parse(result.data);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Acknowledge receipt of operations from a peer.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* await db.acknowledgePeerOperations('peer-123', 10);
|
|
470
|
+
*/
|
|
471
|
+
async acknowledgePeerOperations(peerId, sequence) {
|
|
472
|
+
const result = this.jsDb.acknowledge_peer_operations(peerId, sequence);
|
|
473
|
+
if (!result.success) {
|
|
474
|
+
throw new Error(result.error || 'Failed to acknowledge operations');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get the instance ID for this FeltDB instance.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* const id = db.instanceId();
|
|
482
|
+
*/
|
|
483
|
+
instanceId() {
|
|
484
|
+
return this.jsDb.instance_id();
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get the current sequence number for this instance.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* const seq = db.getSequence();
|
|
491
|
+
*/
|
|
492
|
+
getSequence() {
|
|
493
|
+
return this.jsDb.get_sequence();
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Register a trigger that maps operations to executions
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* await db.registerTrigger({
|
|
500
|
+
* triggerId: "order-created",
|
|
501
|
+
* capability: "process_order",
|
|
502
|
+
* eventType: "OrderCreated",
|
|
503
|
+
* collection: "orders"
|
|
504
|
+
* });
|
|
505
|
+
*/
|
|
506
|
+
async registerTrigger(trigger) {
|
|
507
|
+
// Placeholder for execution trigger registration
|
|
508
|
+
// In full implementation, this would call into the Rust backend
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Register a cron schedule
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* await db.scheduleCron({
|
|
515
|
+
* cronId: "daily-reports",
|
|
516
|
+
* name: "Daily Reports",
|
|
517
|
+
* cronExpr: "0 0 * * *",
|
|
518
|
+
* capability: "generate_reports"
|
|
519
|
+
* });
|
|
520
|
+
*/
|
|
521
|
+
async scheduleCron(schedule) {
|
|
522
|
+
// Placeholder for cron scheduling
|
|
523
|
+
// In full implementation, this would call into the Rust backend
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get pending executions
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* const pending = await db.getPendingExecutions();
|
|
530
|
+
*/
|
|
531
|
+
async getPendingExecutions() {
|
|
532
|
+
// Placeholder for retrieving pending executions
|
|
533
|
+
// In full implementation, this would call into the Rust backend
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Mark an execution as complete
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* await db.completeExecution("exec-1", { success: true });
|
|
541
|
+
*/
|
|
542
|
+
async completeExecution(executionId, result) {
|
|
543
|
+
// Placeholder for marking execution as complete
|
|
544
|
+
// In full implementation, this would call into the Rust backend
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Get the causal graph (provenance) for a given flow reference.
|
|
548
|
+
*
|
|
549
|
+
* Shows how a record was created, what operations it depended on,
|
|
550
|
+
* which workflows/capabilities produced it, etc.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* const graph = db.provenance('flow://documents/report-123');
|
|
554
|
+
* console.log(graph.root); // The record being inspected
|
|
555
|
+
* console.log(graph.edges); // Relationships showing causality
|
|
556
|
+
*/
|
|
557
|
+
provenance(flowRef) {
|
|
558
|
+
// Parse the flow reference
|
|
559
|
+
const [namespace, ...rest] = flowRef.split('://')[1]?.split('/') || [];
|
|
560
|
+
const id = rest.join('/');
|
|
561
|
+
// Create the root node
|
|
562
|
+
const root = {
|
|
563
|
+
id: flowRef,
|
|
564
|
+
type: 'Record',
|
|
565
|
+
label: id || flowRef,
|
|
566
|
+
created_ms: Date.now(),
|
|
567
|
+
};
|
|
568
|
+
// Return a basic provenance graph
|
|
569
|
+
// In full implementation, this would reconstruct the causal history
|
|
570
|
+
// from the operation log and workflow executions
|
|
571
|
+
return {
|
|
572
|
+
root,
|
|
573
|
+
nodes: [root],
|
|
574
|
+
edges: [],
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get runtime health and diagnostics information.
|
|
579
|
+
*
|
|
580
|
+
* Provides a comprehensive view of system health across all components.
|
|
581
|
+
* Useful for debugging distributed issues and monitoring system status.
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* const health = db.health();
|
|
585
|
+
* if (health.status === 'unhealthy') {
|
|
586
|
+
* for (const issue of health.issues) {
|
|
587
|
+
* console.warn(`${issue.component}: ${issue.message}`);
|
|
588
|
+
* }
|
|
589
|
+
* }
|
|
590
|
+
*/
|
|
591
|
+
health() {
|
|
592
|
+
const syncState = this.sync();
|
|
593
|
+
const runtime = this.runtime();
|
|
594
|
+
// Determine sync health
|
|
595
|
+
const syncHealthy = syncState.is_connected && syncState.pending_operations === 0;
|
|
596
|
+
const syncStatus = syncHealthy ? 'healthy' : 'degraded';
|
|
597
|
+
// Determine overall health
|
|
598
|
+
const issues = [];
|
|
599
|
+
if (syncState.pending_operations > 0) {
|
|
600
|
+
issues.push({
|
|
601
|
+
severity: 'info',
|
|
602
|
+
component: 'sync',
|
|
603
|
+
message: `${syncState.pending_operations} pending operations`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (!syncState.is_connected) {
|
|
607
|
+
issues.push({
|
|
608
|
+
severity: 'warning',
|
|
609
|
+
component: 'sync',
|
|
610
|
+
message: 'Network disconnected',
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
if (syncState.conflicts_detected > 0) {
|
|
614
|
+
issues.push({
|
|
615
|
+
severity: 'warning',
|
|
616
|
+
component: 'sync',
|
|
617
|
+
message: `${syncState.conflicts_detected} conflicts detected`,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
const hasIssues = issues.length > 0;
|
|
621
|
+
const overallStatus = hasIssues ? 'degraded' : 'healthy';
|
|
622
|
+
return {
|
|
623
|
+
runtime: {
|
|
624
|
+
status: 'healthy',
|
|
625
|
+
wasm: runtime.runtime === 'wasm' || runtime.runtime === 'browser',
|
|
626
|
+
reactive: runtime.reactive,
|
|
627
|
+
},
|
|
628
|
+
storage: {
|
|
629
|
+
status: 'healthy',
|
|
630
|
+
backend: runtime.storage,
|
|
631
|
+
persistent: runtime.persistent,
|
|
632
|
+
durable: runtime.durable,
|
|
633
|
+
},
|
|
634
|
+
sync: {
|
|
635
|
+
status: syncStatus,
|
|
636
|
+
connected: syncState.is_connected,
|
|
637
|
+
peers: syncState.connected_peers.length,
|
|
638
|
+
pendingOperations: syncState.pending_operations,
|
|
639
|
+
},
|
|
640
|
+
fabric: {
|
|
641
|
+
status: 'healthy',
|
|
642
|
+
references: 0, // Would be populated from Rust backend
|
|
643
|
+
peers: syncState.connected_peers.length,
|
|
644
|
+
},
|
|
645
|
+
capabilities: {
|
|
646
|
+
status: 'healthy',
|
|
647
|
+
count: 0, // Would be populated from Rust backend
|
|
648
|
+
available: 0,
|
|
649
|
+
},
|
|
650
|
+
execution: {
|
|
651
|
+
status: 'healthy',
|
|
652
|
+
pending: 0, // Would be populated from Rust backend
|
|
653
|
+
running: 0,
|
|
654
|
+
failed: 0,
|
|
655
|
+
},
|
|
656
|
+
workflow: {
|
|
657
|
+
status: 'healthy',
|
|
658
|
+
total: 0, // Would be populated from Rust backend
|
|
659
|
+
active: 0,
|
|
660
|
+
},
|
|
661
|
+
status: overallStatus,
|
|
662
|
+
issues,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Open a state-first database instance.
|
|
668
|
+
*
|
|
669
|
+
* The database automatically handles:
|
|
670
|
+
* - Persistence (transparent to application)
|
|
671
|
+
* - Reactive updates (live queries by default)
|
|
672
|
+
* - Relationship loading
|
|
673
|
+
* - Differential updates (single record mutations don't re-emit entire collection)
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* const db = await open(jsDbInstance);
|
|
677
|
+
* const users = db.collection("users");
|
|
678
|
+
* users.subscribe((users) => render(users));
|
|
679
|
+
*
|
|
680
|
+
* @param jsDb The underlying JsDb WASM instance
|
|
681
|
+
* @returns A new StateFirstDB instance
|
|
682
|
+
*/
|
|
683
|
+
export async function open(jsDb) {
|
|
684
|
+
return new StateFirstDB(jsDb);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Helper to create a relationship between two collections.
|
|
688
|
+
*
|
|
689
|
+
* @example
|
|
690
|
+
* const projectTasks = new Relationship(
|
|
691
|
+
* db.collection("tasks"),
|
|
692
|
+
* (task) => task.projectId
|
|
693
|
+
* );
|
|
694
|
+
*
|
|
695
|
+
* const tasks = await projectTasks.load(projectId);
|
|
696
|
+
*/
|
|
697
|
+
export function createRelationship(childCollection, foreignKey) {
|
|
698
|
+
return new Relationship(null, // We'll handle this differently in the new API
|
|
699
|
+
null, 'dummy', foreignKey);
|
|
700
|
+
}
|