@electric-ax/agents-server 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +177 -0
  2. package/dist/chunk-Cl8Af3a2.js +11 -0
  3. package/dist/entrypoint.js +7319 -0
  4. package/dist/index.cjs +7090 -0
  5. package/dist/index.d.cts +4262 -0
  6. package/dist/index.d.ts +4263 -0
  7. package/dist/index.js +7053 -0
  8. package/drizzle/0000_baseline.sql +97 -0
  9. package/drizzle/0001_entity_tags_and_bridges.sql +45 -0
  10. package/drizzle/0002_tag_outbox_hardening.sql +14 -0
  11. package/drizzle/0003_entity_manifest_sources.sql +11 -0
  12. package/drizzle/0004_tenant_scoping.sql +139 -0
  13. package/drizzle/0005_pull_wake_control_plane.sql +156 -0
  14. package/drizzle/meta/0000_snapshot.json +593 -0
  15. package/drizzle/meta/_journal.json +48 -0
  16. package/package.json +89 -0
  17. package/src/authenticated-user-format.ts +17 -0
  18. package/src/claim-write-token-store.ts +74 -0
  19. package/src/db/index.ts +53 -0
  20. package/src/db/schema.ts +490 -0
  21. package/src/dev-asserted-auth.ts +46 -0
  22. package/src/dispatch-policy-schema.ts +52 -0
  23. package/src/electric-agents/adapter-types.ts +70 -0
  24. package/src/electric-agents/default-entity-schemas.ts +1 -0
  25. package/src/electric-agents/schema-validator.ts +143 -0
  26. package/src/electric-agents-http.ts +46 -0
  27. package/src/electric-agents-types.ts +335 -0
  28. package/src/entity-bridge-manager.ts +694 -0
  29. package/src/entity-manager.ts +2601 -0
  30. package/src/entity-projector.ts +765 -0
  31. package/src/entity-registry.ts +1162 -0
  32. package/src/entrypoint-lib.ts +295 -0
  33. package/src/entrypoint.ts +11 -0
  34. package/src/host.ts +323 -0
  35. package/src/index.ts +49 -0
  36. package/src/manifest-side-effects.ts +183 -0
  37. package/src/routing/agent-ui-router.ts +81 -0
  38. package/src/routing/context.ts +35 -0
  39. package/src/routing/cron-router.ts +45 -0
  40. package/src/routing/dispatch-policy.ts +248 -0
  41. package/src/routing/durable-streams-router.ts +407 -0
  42. package/src/routing/durable-streams-routing-adapter.ts +96 -0
  43. package/src/routing/electric-proxy-router.ts +61 -0
  44. package/src/routing/entities-router.ts +484 -0
  45. package/src/routing/entity-types-router.ts +229 -0
  46. package/src/routing/global-router.ts +33 -0
  47. package/src/routing/hooks.ts +123 -0
  48. package/src/routing/internal-router.ts +741 -0
  49. package/src/routing/oss-server-router.ts +56 -0
  50. package/src/routing/runners-router.ts +416 -0
  51. package/src/routing/schema.ts +141 -0
  52. package/src/routing/stream-append.ts +196 -0
  53. package/src/routing/tenant-stream-paths.ts +26 -0
  54. package/src/runtime-registry.ts +49 -0
  55. package/src/runtime.ts +537 -0
  56. package/src/scheduler.ts +788 -0
  57. package/src/schema-validation.ts +15 -0
  58. package/src/server.ts +374 -0
  59. package/src/standalone-runtime.ts +188 -0
  60. package/src/stream-client.ts +842 -0
  61. package/src/tag-stream-outbox-drainer.ts +188 -0
  62. package/src/tenant.ts +25 -0
  63. package/src/tracing.ts +57 -0
  64. package/src/utils/electric-url.ts +15 -0
  65. package/src/utils/log.ts +95 -0
  66. package/src/utils/server-utils.ts +245 -0
  67. package/src/utils/webhook-url.ts +33 -0
  68. package/src/wake-registry.ts +946 -0
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@electric-ax/agents-server",
3
+ "version": "0.3.0",
4
+ "description": "Electric Agents entity runtime server",
5
+ "author": "Durable Stream contributors",
6
+ "bin": {
7
+ "electric-agents-server": "./dist/entrypoint.js",
8
+ "durable-streams-electric-agents-server": "./dist/entrypoint.js"
9
+ },
10
+ "license": "Apache-2.0",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/electric-sql/electric.git",
14
+ "directory": "packages/agents-server"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "type": "module",
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "require": {
30
+ "types": "./dist/index.d.cts",
31
+ "default": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "sideEffects": false,
37
+ "dependencies": {
38
+ "@anthropic-ai/sdk": "^0.78.0",
39
+ "@durable-streams/client": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/client@350",
40
+ "@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@350",
41
+ "@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@350",
42
+ "@electric-sql/client": "^1.5.17",
43
+ "@mariozechner/pi-agent-core": "^0.70.2",
44
+ "@opentelemetry/api": "^1.9.1",
45
+ "@sinclair/typebox": "^0.34.48",
46
+ "@whatwg-node/server": "^0.10.18",
47
+ "ajv": "^8.18.0",
48
+ "cron-parser": "^5.5.0",
49
+ "drizzle-orm": "^0.44.0",
50
+ "fastq": "^1.20.1",
51
+ "itty-router": "^5.0.23",
52
+ "lmdb": "^3.5.1",
53
+ "pino": "^10.3.1",
54
+ "pino-pretty": "^13.0.0",
55
+ "postgres": "^3.4.0",
56
+ "undici": "^7.24.7",
57
+ "@electric-ax/agents-runtime": "0.1.3"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.19.15",
61
+ "@vitest/coverage-v8": "^4.1.0",
62
+ "cross-env": "^10.1.0",
63
+ "drizzle-kit": "^0.31.0",
64
+ "tsdown": "^0.9.0",
65
+ "tsx": "^4.19.0",
66
+ "typescript": "^5.0.0",
67
+ "vitest": "^4.1.0",
68
+ "@electric-ax/agents-server-conformance-tests": "0.1.2",
69
+ "@electric-ax/agents": "0.3.0",
70
+ "@electric-ax/agents-server-ui": "0.3.0"
71
+ },
72
+ "files": [
73
+ "dist",
74
+ "drizzle",
75
+ "src"
76
+ ],
77
+ "engines": {
78
+ "node": ">=18.0.0"
79
+ },
80
+ "scripts": {
81
+ "build": "tsdown",
82
+ "dev": "tsdown --watch",
83
+ "start": "cross-env-shell \"DATABASE_URL=${DATABASE_URL:-postgresql://electric_agents:electric_agents@localhost:5432/electric_agents} ELECTRIC_URL=${ELECTRIC_URL:-http://localhost:3060} tsx --watch src/entrypoint.ts\"",
84
+ "test": "vitest run",
85
+ "coverage": "ELECTRIC_AGENTS_KEEP_BACKEND=1 pnpm exec vitest run --coverage $(find test -name '*.test.ts' ! -name 'conformance.test.ts' | sort) && pnpm exec vitest run test/conformance.test.ts",
86
+ "typecheck": "tsc --noEmit",
87
+ "stylecheck": "eslint . --quiet"
88
+ }
89
+ }
@@ -0,0 +1,17 @@
1
+ import type { AuthenticatedRequestUser } from './electric-agents-types.js'
2
+
3
+ function clean(value: string | undefined): string | undefined {
4
+ const trimmed = value?.trim()
5
+ return trimmed || undefined
6
+ }
7
+
8
+ export function formatAuthenticatedUser(
9
+ user: AuthenticatedRequestUser | null | undefined
10
+ ): string | undefined {
11
+ if (!user) return undefined
12
+ const email = clean(user.email)
13
+ const name = clean(user.name)
14
+ const userId = clean(user.userId)
15
+ if (name && email) return `${name} <${email}>`
16
+ return email ?? name ?? userId
17
+ }
@@ -0,0 +1,74 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ interface ActiveClaimWriteToken {
4
+ token: string
5
+ consumerId: string
6
+ }
7
+
8
+ export class ClaimWriteTokenStore {
9
+ private readonly claimsByStream = new Map<string, ActiveClaimWriteToken>()
10
+ private readonly streamByConsumer = new Map<string, string>()
11
+
12
+ mint(service: string, streamPath: string, consumerId: string): string {
13
+ const streamKey = this.streamKey(service, streamPath)
14
+ const consumerKey = this.consumerKey(service, consumerId)
15
+ const previousClaimForStream = this.claimsByStream.get(streamKey)
16
+ if (previousClaimForStream) {
17
+ this.streamByConsumer.delete(
18
+ this.consumerKey(service, previousClaimForStream.consumerId)
19
+ )
20
+ }
21
+
22
+ const previousStreamForConsumer = this.streamByConsumer.get(consumerKey)
23
+ if (previousStreamForConsumer) {
24
+ this.claimsByStream.delete(previousStreamForConsumer)
25
+ }
26
+
27
+ const token = randomUUID()
28
+ this.claimsByStream.set(streamKey, { token, consumerId })
29
+ this.streamByConsumer.set(consumerKey, streamKey)
30
+ return token
31
+ }
32
+
33
+ isValid(service: string, streamPath: string, token: string): boolean {
34
+ return (
35
+ this.claimsByStream.get(this.streamKey(service, streamPath))?.token ===
36
+ token
37
+ )
38
+ }
39
+
40
+ owns(service: string, streamPath: string, consumerId: string): boolean {
41
+ return (
42
+ this.claimsByStream.get(this.streamKey(service, streamPath))
43
+ ?.consumerId === consumerId
44
+ )
45
+ }
46
+
47
+ clearStream(service: string, streamPath: string): void {
48
+ const streamKey = this.streamKey(service, streamPath)
49
+ const activeClaim = this.claimsByStream.get(streamKey)
50
+ if (!activeClaim) return
51
+
52
+ this.claimsByStream.delete(streamKey)
53
+ this.streamByConsumer.delete(
54
+ this.consumerKey(service, activeClaim.consumerId)
55
+ )
56
+ }
57
+
58
+ clearConsumer(service: string, consumerId: string): void {
59
+ const consumerKey = this.consumerKey(service, consumerId)
60
+ const streamKey = this.streamByConsumer.get(consumerKey)
61
+ if (!streamKey) return
62
+
63
+ this.streamByConsumer.delete(consumerKey)
64
+ this.claimsByStream.delete(streamKey)
65
+ }
66
+
67
+ private streamKey(service: string, streamPath: string): string {
68
+ return `${service}\0${streamPath}`
69
+ }
70
+
71
+ private consumerKey(service: string, consumerId: string): string {
72
+ return `${service}\0${consumerId}`
73
+ }
74
+ }
@@ -0,0 +1,53 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { drizzle } from 'drizzle-orm/postgres-js'
5
+ import { migrate } from 'drizzle-orm/postgres-js/migrator'
6
+ import postgres from 'postgres'
7
+ import * as schema from './schema.js'
8
+
9
+ export type DrizzleDB = ReturnType<typeof drizzle<typeof schema>>
10
+ export type PgClient = ReturnType<typeof postgres>
11
+
12
+ export function createDb(postgresUrl: string): {
13
+ db: DrizzleDB
14
+ client: PgClient
15
+ } {
16
+ const poolMax = Number(process.env.ELECTRIC_AGENTS_PG_POOL_MAX ?? `100`)
17
+ const client = postgres(postgresUrl, {
18
+ max: poolMax,
19
+ fetch_types: false,
20
+ })
21
+ const db = drizzle(client, { schema })
22
+ return { db, client }
23
+ }
24
+
25
+ export function resolveMigrationsFolder(fromUrl = import.meta.url): string {
26
+ const here = dirname(fileURLToPath(fromUrl))
27
+ const candidates = [
28
+ resolve(here, `../../drizzle`),
29
+ resolve(here, `../drizzle`),
30
+ resolve(process.cwd(), `packages/agents-server/drizzle`),
31
+ ]
32
+
33
+ const folder = candidates.find((candidate) => existsSync(candidate))
34
+ if (!folder) {
35
+ throw new Error(
36
+ `Could not locate agent-server migrations directory from ${fromUrl}`
37
+ )
38
+ }
39
+
40
+ return folder
41
+ }
42
+
43
+ export async function runMigrations(postgresUrl: string): Promise<void> {
44
+ const migrationClient = postgres(postgresUrl, {
45
+ max: 1,
46
+ onnotice: () => {},
47
+ })
48
+ const db = drizzle(migrationClient)
49
+ await migrate(db, {
50
+ migrationsFolder: resolveMigrationsFolder(),
51
+ })
52
+ await migrationClient.end()
53
+ }