@anteros/core 0.0.41 → 0.0.42

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 (102) hide show
  1. package/database/audit.ts +9 -59
  2. package/database/collection.ts +3 -1
  3. package/database/deleteLog.ts +67 -3
  4. package/database/file.ts +4 -3
  5. package/database/mongodbadapter.ts +103 -42
  6. package/database/replication.ts +110 -32
  7. package/database/ttl.ts +68 -0
  8. package/database/workflow.ts +466 -240
  9. package/index.ts +6 -0
  10. package/lib/agent.ts +1327 -0
  11. package/lib/agentMemory.ts +586 -0
  12. package/lib/agents.ts +157 -0
  13. package/lib/ai.ts +0 -0
  14. package/lib/api.ts +246 -0
  15. package/lib/attachment.ts +351 -0
  16. package/lib/auditResult.ts +13 -0
  17. package/lib/define.ts +43 -1
  18. package/lib/endpoints.ts +80 -0
  19. package/lib/files.ts +2 -1
  20. package/lib/jsonSchema.ts +183 -0
  21. package/lib/lifecycle.ts +7 -1
  22. package/lib/load.ts +17 -0
  23. package/lib/mcp.ts +3 -2
  24. package/lib/messages.ts +299 -0
  25. package/lib/providers.ts +867 -0
  26. package/lib/routes.ts +14 -1
  27. package/lib/scriptWorker.ts +4 -1
  28. package/lib/scripts.ts +6 -1
  29. package/lib/sockets.ts +3 -1
  30. package/lib/workflow.ts +209 -37
  31. package/lib/zip.ts +109 -0
  32. package/package.json +1 -1
  33. package/server/access.ts +96 -0
  34. package/server/agents.ts +451 -0
  35. package/server/api.ts +14 -72
  36. package/server/boot.ts +11 -0
  37. package/server/hono.ts +8 -3
  38. package/server/mcp.ts +17 -63
  39. package/server/routes.ts +19 -24
  40. package/tests/agent-memory.test.ts +617 -0
  41. package/tests/agent-working-memory.test.ts +402 -0
  42. package/tests/agent.test.ts +1239 -0
  43. package/tests/agents-api.test.ts +593 -0
  44. package/tests/api.test.ts +51 -0
  45. package/tests/attachment.test.ts +241 -0
  46. package/tests/audit-operations.test.ts +1 -1
  47. package/tests/audit-redact.test.ts +29 -3
  48. package/tests/audit-run-action.test.ts +1 -1
  49. package/tests/endpoints.test.ts +80 -0
  50. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  51. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  52. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  53. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  54. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  55. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  56. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  57. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  58. package/tests/fixtures/attachment/data.csv +2 -0
  59. package/tests/fixtures/fake-provider.ts +128 -0
  60. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  61. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  62. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  63. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  64. package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
  65. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  66. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  67. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  68. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  69. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  70. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  71. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  72. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  73. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  74. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  75. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  76. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  77. package/tests/hono.test.ts +6 -3
  78. package/tests/locks.test.ts +120 -0
  79. package/tests/mcp.test.ts +11 -11
  80. package/tests/messages.test.ts +258 -0
  81. package/tests/replication-control.test.ts +2 -2
  82. package/tests/replication-meta.test.ts +243 -0
  83. package/tests/upload.test.ts +44 -12
  84. package/tests/vars-api.test.ts +1 -1
  85. package/tests/vars-replication.test.ts +3 -2
  86. package/tests/workflow-engine.test.ts +340 -0
  87. package/tests/workflow-retention.test.ts +103 -0
  88. package/tests/workflow.test.ts +186 -0
  89. package/types/agent.d.ts +837 -0
  90. package/types/config.d.ts +16 -0
  91. package/types/file.d.ts +7 -7
  92. package/types/hook.d.ts +6 -0
  93. package/types/lifecycle.d.ts +6 -0
  94. package/types/mcp.d.ts +11 -1
  95. package/types/replication.d.ts +29 -0
  96. package/types/route.d.ts +7 -1
  97. package/types/scripts.d.ts +6 -0
  98. package/types/service.d.ts +7 -1
  99. package/types/tenant.d.ts +13 -0
  100. package/types/vars.d.ts +2 -2
  101. package/types/websocket.d.ts +6 -0
  102. package/types/workflow.d.ts +50 -3
package/lib/agents.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Glob } from "bun";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+ import { cfg } from "../server/config";
5
+ import { Agent } from "./agent";
6
+ import { InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory } from "./agentMemory";
7
+ import { importDefinition } from "./load";
8
+ import { logger } from "../utils/logger";
9
+ import type { useRest } from "../database/rest";
10
+ import type { AgentDefinition, TenantAgents } from "../types/agent";
11
+
12
+ /**
13
+ * Agent loader — `{tenant.dir}/agents/**\/*.agent.ts`.
14
+ *
15
+ * Each definition is instantiated once per tenant and kept in a process-wide
16
+ * registry keyed by `tenant:agent`. `agents.get('weather')` resolves it at
17
+ * runtime, binding the *calling* `rest` so tools run with the right scope.
18
+ */
19
+
20
+ const registry = new Map<string, Agent>();
21
+ const definitions: AgentDefinition[] = [];
22
+
23
+ async function syncAgents(): Promise<void> {
24
+ registry.clear();
25
+ definitions.length = 0;
26
+ const memories: Array<{ _tenant_: string; collection: string }> = [];
27
+
28
+ for (const tenant of cfg.tenants ?? []) {
29
+ const agentsPath = path.join(process.cwd(), tenant.dir, "agents");
30
+ if (!(await fs.exists(agentsPath))) continue;
31
+ if (!(await fs.stat(agentsPath)).isDirectory()) continue;
32
+
33
+ const glob = new Glob(path.join(agentsPath, "**/*.agent.ts"));
34
+ for await (const file of glob.scan(".")) {
35
+ const module = await importDefinition(file, "Agent");
36
+ const definition: AgentDefinition | undefined = module?.default;
37
+ if (!definition?._isAgent_) continue;
38
+ if (definition.enabled === false) continue;
39
+
40
+ // `id` and `description` are required — the `Agent` constructor refuses a
41
+ // definition without them, and a broken file never hides the others
42
+ const id = definition.id!;
43
+
44
+ const key = `${tenant.id}:${id}`;
45
+ const existing = registry.get(key);
46
+ if (existing) {
47
+ logger.warn(`Duplicate agent id '${id}' for tenant '${tenant.id}' — '${file}' replaces the previous definition`);
48
+ }
49
+
50
+ try {
51
+ const instance = new Agent({ ...definition, id }, { tenant: tenant.id });
52
+ registry.set(key, instance);
53
+ definitions.push(instance.getConfig());
54
+
55
+ // A store says which collections it writes to (a Mongo memory does):
56
+ // published on `cfg` so the replication engine copies them, exactly
57
+ // like a declared collection — the tenant chooses the name.
58
+ for (const collection of instance.getMemory()?.collections?.() ?? []) {
59
+ if (!memories.some((m) => m._tenant_ === tenant.id && m.collection === collection)) {
60
+ memories.push({ _tenant_: tenant.id, collection });
61
+ }
62
+ }
63
+ } catch (err: any) {
64
+ console.error(`Failed to load agent '${id}' of tenant '${tenant.id}': ${err?.message}`);
65
+ }
66
+ }
67
+ }
68
+
69
+ cfg.agents = definitions;
70
+ cfg.agentMemories = memories;
71
+ }
72
+
73
+ /**
74
+ * The agents declared by a tenant, ready to run.
75
+ *
76
+ * When a `rest` is given (always the case through the `agents` member of a
77
+ * context), every call
78
+ * returns a **copy** bound to it. The registry instance is shared by every
79
+ * caller — binding it in place would let one request's client (and its Mongo
80
+ * session, hence its transaction) leak into another request running at the same
81
+ * time. A copy shares the declaration (instructions, tools, memory), so nothing
82
+ * else changes.
83
+ */
84
+ function createAgents(tenantId: string, rest?: InstanceType<typeof useRest> | any): TenantAgents {
85
+ const prefix = `${tenantId}:`;
86
+ const bind = (agent: Agent): Agent => rest
87
+ ? new Agent(agent.getConfig(), { rest, tenant: agent.getTenant() })
88
+ : agent;
89
+ const entries = (): Array<[string, Agent]> =>
90
+ [...registry.entries()].filter(([key]) => key.startsWith(prefix));
91
+
92
+ return {
93
+ get: (id) => {
94
+ const agent = registry.get(`${prefix}${id}`);
95
+ return agent ? bind(agent) : undefined;
96
+ },
97
+ has: (id) => registry.has(`${prefix}${id}`),
98
+ ids: () => entries().map(([key]) => key.slice(prefix.length)),
99
+ list: () => entries().map(([, agent]) => bind(agent)),
100
+ reload: syncAgents,
101
+ };
102
+ }
103
+
104
+ /** Boot banner counters. */
105
+ function agentsStats(): { total: number; tenants: string[] } {
106
+ const tenants = new Set([...registry.keys()].map((key) => key.slice(0, key.indexOf(":"))));
107
+ return { total: registry.size, tenants: [...tenants] };
108
+ }
109
+
110
+ /**
111
+ * Process-wide agent registry, importable from **anywhere** — including a place
112
+ * with no `rest` at hand: module scope, a global middleware, a `Bun.cron`, a
113
+ * `Bun.serve` of your own.
114
+ *
115
+ * ```ts
116
+ * import { agents } from '@anteros/core'
117
+ *
118
+ * const support = agents.get('v1', 'support')
119
+ * await support.generate('Where is my order?')
120
+ * ```
121
+ *
122
+ * Prefer the `agents` member of a context whenever there is one (a script, a
123
+ * service action, a hook, a socket, a lifecycle hook, a workflow step, a route,
124
+ * an MCP tool): the tenant is implicit and the **calling** client is bound, so the
125
+ * agent's tools run with the right scope. Without a `rest`, pass one as the third
126
+ * argument (`agents.get(tenantId, id, rest)`) or the tools run without a database
127
+ * client.
128
+ */
129
+ const agents = {
130
+ /** The agent, bound to `rest` when provided — `undefined` if it does not exist. */
131
+ get: (tenantId: string, id: string, rest?: InstanceType<typeof useRest>): Agent | undefined =>
132
+ createAgents(tenantId, rest).get(id),
133
+ has: (tenantId: string, id: string): boolean => registry.has(`${tenantId}:${id}`),
134
+ /** Declared agent ids of a tenant. */
135
+ ids: (tenantId: string): string[] => createAgents(tenantId).ids(),
136
+ list: (tenantId: string, rest?: InstanceType<typeof useRest>): Agent[] => createAgents(tenantId, rest).list(),
137
+ /** Tenant-scoped registry — the same object a context receives as `agents`. */
138
+ use: createAgents,
139
+ /** Re-scan `{tenant.dir}/agents` and rebuild the registry. */
140
+ reload: syncAgents,
141
+ /** `{ total, tenants }` — what the boot banner prints. */
142
+ stats: agentsStats,
143
+ memory: {
144
+ InMemory: InMemoryAgentMemory,
145
+ Mongo: MongoAgentMemory,
146
+ Redis: RedisAgentMemory,
147
+ },
148
+ };
149
+
150
+ /** Mongo collections used as agent memory by a tenant — published by the loader. */
151
+ function agentMemoryCollections(tenantId: string): string[] {
152
+ return (cfg.agentMemories ?? [])
153
+ .filter((memory) => memory._tenant_ === tenantId)
154
+ .map((memory) => memory.collection);
155
+ }
156
+
157
+ export { syncAgents, createAgents, agentsStats, agents, agentMemoryCollections, InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory };
package/lib/ai.ts ADDED
File without changes
package/lib/api.ts ADDED
@@ -0,0 +1,246 @@
1
+ import { createAgents } from "./agents";
2
+ import { endpoints } from "./endpoints";
3
+ import { handleDelete } from "./files";
4
+ import type { useRest } from "../database/rest";
5
+ import type { Agent } from "./agent";
6
+ import type { FindCallOptions, FindOptions, findOneOptions } from "../types/mongo";
7
+ import type { updateApiOptions } from "../types/api";
8
+ import type { TenantVars } from "../types/vars";
9
+ import type { BulkUpdateOperation } from "../types/rest";
10
+ import type {
11
+ AnyBulkWriteOperation,
12
+ BulkWriteOptions,
13
+ BulkWriteResult,
14
+ ChangeStream,
15
+ ChangeStreamOptions,
16
+ DeleteResult,
17
+ Document,
18
+ UpdateFilter,
19
+ UpdateResult,
20
+ } from "mongodb";
21
+
22
+ type Rest = InstanceType<typeof useRest>;
23
+
24
+ /**
25
+ * The in-process facade — an `api` member in every context, next to `rest`,
26
+ * `io` and `agents`.
27
+ *
28
+ * It is **the same thing as `rest`, grouped the way the SDK and the HTTP API are
29
+ * grouped**: one namespace per family, and a **bound client** for a collection.
30
+ * Nothing is re-implemented — every call lands on the very same `rest` method the
31
+ * context already had, so audit, hooks, access rules, the session and the
32
+ * transaction are those of the caller.
33
+ *
34
+ * ```ts
35
+ * export default define.Hook(async ({ api, rest }) => {
36
+ * // Bound to one collection — the slug is stated once
37
+ * const orders = api.collection('orders');
38
+ * const [order] = await orders.find({ $match: { status: 'paid' }, $limit: 1 });
39
+ * await orders.updateOne(order._id, { $set: { status: 'shipped' } });
40
+ *
41
+ * // The families
42
+ * await api.vars.set('config', 'lastRun', new Date().toISOString());
43
+ * await api.service('analytics').run('refresh', { orderId: order._id });
44
+ * const url = api.files.url('invoices', 'f1.pdf', { width: 800 });
45
+ * await api.agent('triage')?.generate(`Classify: ${order.ref}`);
46
+ *
47
+ * // …and `rest` stays exactly what it was
48
+ * await rest.aggregate('orders', [{ $count: 'total' }]);
49
+ * });
50
+ * ```
51
+ *
52
+ * Naming rule, shared with the SDK: **singular = a client bound to one resource**
53
+ * (`api.collection(slug)`, `api.agent(id)`), **plural = the family's operations**
54
+ * (`api.vars`, `api.files`).
55
+ */
56
+
57
+ /** One collection, bound to the `rest` of the context. */
58
+ class ApiCollection<T = any> {
59
+ #rest: Rest;
60
+ #slug: string;
61
+
62
+ constructor(rest: Rest, slug: string) {
63
+ this.#rest = rest;
64
+ this.#slug = slug;
65
+ }
66
+
67
+ /** The slug this client is bound to. */
68
+ getSlug(): string {
69
+ return this.#slug;
70
+ }
71
+
72
+ // ── Reads ────────────────────────────────────────────────────────────
73
+
74
+ find(params: FindOptions = {}, options: FindCallOptions = {}): Promise<Document[]> {
75
+ return this.#rest.find(this.#slug, params, options);
76
+ }
77
+
78
+ findOne(_id: string, params?: findOneOptions): Promise<any> {
79
+ return this.#rest.findOne(this.#slug, _id, params);
80
+ }
81
+
82
+ aggregate<T = Document>(pipeline: any[]): Promise<T[]> {
83
+ return this.#rest.aggregate(this.#slug, pipeline) as Promise<T[]>;
84
+ }
85
+
86
+ countDocuments(query: any = {}): Promise<number> {
87
+ return this.#rest.countDocuments(this.#slug, query);
88
+ }
89
+
90
+ /** The collection's change stream — the caller consumes it. */
91
+ watch(pipeline: any[], options: ChangeStreamOptions): Promise<ChangeStream> {
92
+ return this.#rest.watch(this.#slug, pipeline, options);
93
+ }
94
+
95
+ // ── Writes ───────────────────────────────────────────────────────────
96
+
97
+ insertOne<TBody>(data: TBody): Promise<TBody & { _id: string }> {
98
+ return this.#rest.insertOne(this.#slug, data);
99
+ }
100
+
101
+ insertMany<TBody>(data: TBody[]): Promise<(TBody & { _id: string })[]> {
102
+ return this.#rest.insertMany(this.#slug, data);
103
+ }
104
+
105
+ updateOne(_id: string, update: UpdateFilter<any>): Promise<UpdateResult & { previous?: any }> {
106
+ return this.#rest.updateOne(this.#slug, _id, update) as Promise<UpdateResult & { previous?: any }>;
107
+ }
108
+
109
+ findOneAndUpdate(
110
+ filter: Document,
111
+ update: UpdateFilter<any>,
112
+ options?: updateApiOptions,
113
+ ): Promise<Document | null> {
114
+ return this.#rest.findOneAndUpdate(this.#slug, filter, update, options);
115
+ }
116
+
117
+ updateMany(_ids: string[], update: UpdateFilter<any>): Promise<UpdateResult> {
118
+ return this.#rest.updateMany(this.#slug, _ids, update);
119
+ }
120
+
121
+ deleteOne(_id: string): Promise<any> {
122
+ return this.#rest.deleteOne(this.#slug, _id);
123
+ }
124
+
125
+ deleteMany(_ids: string[]): Promise<DeleteResult> {
126
+ return this.#rest.deleteMany(this.#slug, _ids);
127
+ }
128
+
129
+ bulkWrite(operations: AnyBulkWriteOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
130
+ return this.#rest.bulkWrite(this.#slug, operations, options);
131
+ }
132
+
133
+ bulkUpdate(operations: BulkUpdateOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
134
+ return this.#rest.bulkUpdate(this.#slug, operations, options);
135
+ }
136
+
137
+ // ── Maintenance & custom actions ─────────────────────────────────────
138
+
139
+ dropCollection(): Promise<any> {
140
+ return this.#rest.dropCollection(this.#slug);
141
+ }
142
+
143
+ dropIndex(index: string): Promise<any> {
144
+ return this.#rest.dropIndex(this.#slug, index);
145
+ }
146
+
147
+ dropIndexes(): Promise<any> {
148
+ return this.#rest.dropIndexes(this.#slug);
149
+ }
150
+
151
+ /** A custom action declared on the collection (`define.Action` / `actions`). */
152
+ runAction<T = any>(action: string, data?: any): Promise<T> {
153
+ return this.#rest.runAction<T>(this.#slug, action, data);
154
+ }
155
+ }
156
+
157
+ /** One service of the tenant, bound to its name — `api.service('analytics')`. */
158
+ class ApiService {
159
+ #rest: Rest;
160
+ #name: string;
161
+
162
+ constructor(rest: Rest, name: string) {
163
+ this.#rest = rest;
164
+ this.#name = name;
165
+ }
166
+
167
+ /** The service name this client is bound to. */
168
+ getName(): string {
169
+ return this.#name;
170
+ }
171
+
172
+ /** Run one action of the service — `data` is what the handler reads as `body.data`. */
173
+ run<T = any>(action: string, data?: any): Promise<T> {
174
+ return this.#rest.runService<T>(this.#name, action, data);
175
+ }
176
+ }
177
+
178
+ /** The `api` member of a context — see the class doc for the convention. */
179
+ type Api = {
180
+ /** One collection, bound: `api.collection('orders').find(...)`. */
181
+ collection<T = any>(slug: string): ApiCollection<T>;
182
+ /** The tenant's variables — the same object as `rest.vars`. */
183
+ vars: TenantVars;
184
+ /** One service, bound: `api.service('analytics').run('monthly', data)`. */
185
+ service<T = any>(name: string): ApiService & { run<R = T>(action: string, data?: any): Promise<R> };
186
+ /** File collections — the URL of a stored file, and its deletion. */
187
+ files: {
188
+ url(collection: string, filename: string, transform?: {
189
+ width?: number;
190
+ height?: number;
191
+ format?: "webp" | "jpeg" | "png" | "avif";
192
+ quality?: number;
193
+ }): string;
194
+ delete(collection: string, fileId: string): Promise<{ message: string; ok: boolean }>;
195
+ };
196
+ /** One agent of the tenant, bound to the same `rest` — `undefined` if unknown. */
197
+ agent(id: string): Agent | undefined;
198
+ };
199
+
200
+ /**
201
+ * Build the `api` facade of a context. Everything delegates to `rest`, so a hook,
202
+ * a script or a workflow step gets the same client it already had — only the shape
203
+ * changes.
204
+ */
205
+ function createApi(rest: Rest): Api {
206
+ const tenant = rest.tenant_id;
207
+ const registry = createAgents(tenant, rest);
208
+
209
+ return {
210
+ collection: <T = any>(slug: string) => new ApiCollection<T>(rest, slug),
211
+ // `rest.vars` returns a fresh bound client on each access — keep that
212
+ get vars() {
213
+ return rest.vars;
214
+ },
215
+ service: (name: string) => new ApiService(rest, name),
216
+ files: {
217
+ /** `/api/<tenant>/files/<collection>/<file>` (+ `w`, `h`, `format`, `q`). */
218
+ url(collection: string, filename: string, transform?: {
219
+ width?: number;
220
+ height?: number;
221
+ format?: "webp" | "jpeg" | "png" | "avif";
222
+ quality?: number;
223
+ }): string {
224
+ const base = endpoints.file(tenant, collection, filename);
225
+ if (!transform) return base;
226
+
227
+ const params = new URLSearchParams();
228
+ if (transform.width) params.set("w", String(transform.width));
229
+ if (transform.height) params.set("h", String(transform.height));
230
+ if (transform.format) params.set("format", transform.format);
231
+ if (transform.quality) params.set("q", String(transform.quality));
232
+
233
+ const query = params.toString();
234
+ return query ? `${base}?${query}` : base;
235
+ },
236
+ /** Removes the stored binary **and** the document (the route does the same). */
237
+ delete(collection: string, fileId: string) {
238
+ return handleDelete(tenant, collection, fileId).then(() => ({ message: "File deleted", ok: true }));
239
+ },
240
+ },
241
+ agent: (id: string) => registry.get(id),
242
+ } as Api;
243
+ }
244
+
245
+ export { ApiCollection, ApiService, createApi };
246
+ export type { Api };