@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.
- package/LICENSE +177 -0
- package/dist/chunk-Cl8Af3a2.js +11 -0
- package/dist/entrypoint.js +7319 -0
- package/dist/index.cjs +7090 -0
- package/dist/index.d.cts +4262 -0
- package/dist/index.d.ts +4263 -0
- package/dist/index.js +7053 -0
- package/drizzle/0000_baseline.sql +97 -0
- package/drizzle/0001_entity_tags_and_bridges.sql +45 -0
- package/drizzle/0002_tag_outbox_hardening.sql +14 -0
- package/drizzle/0003_entity_manifest_sources.sql +11 -0
- package/drizzle/0004_tenant_scoping.sql +139 -0
- package/drizzle/0005_pull_wake_control_plane.sql +156 -0
- package/drizzle/meta/0000_snapshot.json +593 -0
- package/drizzle/meta/_journal.json +48 -0
- package/package.json +89 -0
- package/src/authenticated-user-format.ts +17 -0
- package/src/claim-write-token-store.ts +74 -0
- package/src/db/index.ts +53 -0
- package/src/db/schema.ts +490 -0
- package/src/dev-asserted-auth.ts +46 -0
- package/src/dispatch-policy-schema.ts +52 -0
- package/src/electric-agents/adapter-types.ts +70 -0
- package/src/electric-agents/default-entity-schemas.ts +1 -0
- package/src/electric-agents/schema-validator.ts +143 -0
- package/src/electric-agents-http.ts +46 -0
- package/src/electric-agents-types.ts +335 -0
- package/src/entity-bridge-manager.ts +694 -0
- package/src/entity-manager.ts +2601 -0
- package/src/entity-projector.ts +765 -0
- package/src/entity-registry.ts +1162 -0
- package/src/entrypoint-lib.ts +295 -0
- package/src/entrypoint.ts +11 -0
- package/src/host.ts +323 -0
- package/src/index.ts +49 -0
- package/src/manifest-side-effects.ts +183 -0
- package/src/routing/agent-ui-router.ts +81 -0
- package/src/routing/context.ts +35 -0
- package/src/routing/cron-router.ts +45 -0
- package/src/routing/dispatch-policy.ts +248 -0
- package/src/routing/durable-streams-router.ts +407 -0
- package/src/routing/durable-streams-routing-adapter.ts +96 -0
- package/src/routing/electric-proxy-router.ts +61 -0
- package/src/routing/entities-router.ts +484 -0
- package/src/routing/entity-types-router.ts +229 -0
- package/src/routing/global-router.ts +33 -0
- package/src/routing/hooks.ts +123 -0
- package/src/routing/internal-router.ts +741 -0
- package/src/routing/oss-server-router.ts +56 -0
- package/src/routing/runners-router.ts +416 -0
- package/src/routing/schema.ts +141 -0
- package/src/routing/stream-append.ts +196 -0
- package/src/routing/tenant-stream-paths.ts +26 -0
- package/src/runtime-registry.ts +49 -0
- package/src/runtime.ts +537 -0
- package/src/scheduler.ts +788 -0
- package/src/schema-validation.ts +15 -0
- package/src/server.ts +374 -0
- package/src/standalone-runtime.ts +188 -0
- package/src/stream-client.ts +842 -0
- package/src/tag-stream-outbox-drainer.ts +188 -0
- package/src/tenant.ts +25 -0
- package/src/tracing.ts +57 -0
- package/src/utils/electric-url.ts +15 -0
- package/src/utils/log.ts +95 -0
- package/src/utils/server-utils.ts +245 -0
- package/src/utils/webhook-url.ts +33 -0
- package/src/wake-registry.ts +946 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import type { TSchema as TypeBoxSchema } from '@sinclair/typebox'
|
|
3
|
+
import type { ValidateFunction } from 'ajv'
|
|
4
|
+
|
|
5
|
+
const jsonBodyAjv = new Ajv({ allErrors: true })
|
|
6
|
+
const schemaValidators = new WeakMap<TypeBoxSchema, ValidateFunction>()
|
|
7
|
+
|
|
8
|
+
export function schemaValidator(schema: TypeBoxSchema): ValidateFunction {
|
|
9
|
+
let validate = schemaValidators.get(schema)
|
|
10
|
+
if (!validate) {
|
|
11
|
+
validate = jsonBodyAjv.compile(schema)
|
|
12
|
+
schemaValidators.set(schema, validate)
|
|
13
|
+
}
|
|
14
|
+
return validate
|
|
15
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import { createServerAdapter } from '@whatwg-node/server'
|
|
3
|
+
import { Agent } from 'undici'
|
|
4
|
+
import {
|
|
5
|
+
appendPathToUrl,
|
|
6
|
+
createEntityRegistry,
|
|
7
|
+
createRuntimeHandler,
|
|
8
|
+
} from '@electric-ax/agents-runtime'
|
|
9
|
+
import { createDb, runMigrations } from './db/index.js'
|
|
10
|
+
import { pathPrefixedSingleTenantDurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
|
|
11
|
+
import { ossServerRouter } from './routing/oss-server-router.js'
|
|
12
|
+
import { startStandaloneAgentsRuntime } from './standalone-runtime.js'
|
|
13
|
+
import { StreamClient, durableStreamsServiceUrl } from './stream-client.js'
|
|
14
|
+
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
15
|
+
import { serverLog } from './utils/log.js'
|
|
16
|
+
import type { DrizzleDB, PgClient } from './db/index.js'
|
|
17
|
+
import type { Server } from 'node:http'
|
|
18
|
+
import type { DurableStreamTestServer } from '@durable-streams/server'
|
|
19
|
+
import type { StreamFn } from '@mariozechner/pi-agent-core'
|
|
20
|
+
import type {
|
|
21
|
+
AgentModel,
|
|
22
|
+
EntityRegistry,
|
|
23
|
+
RuntimeHandler,
|
|
24
|
+
} from '@electric-ax/agents-runtime'
|
|
25
|
+
import type { AuthenticateRequest } from './electric-agents-types.js'
|
|
26
|
+
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
27
|
+
import type { DurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
|
|
28
|
+
import type { OssServerContext } from './routing/oss-server-router.js'
|
|
29
|
+
import type { StartedStandaloneAgentsRuntime } from './standalone-runtime.js'
|
|
30
|
+
import type { DurableStreamsBearerProvider } from './stream-client.js'
|
|
31
|
+
|
|
32
|
+
const MOCK_AGENT_HANDLER_PATH = `/_electric/mock-agent-handler`
|
|
33
|
+
|
|
34
|
+
export interface ElectricAgentsServerOptions {
|
|
35
|
+
service?: string
|
|
36
|
+
tenantId?: string
|
|
37
|
+
baseUrl?: string
|
|
38
|
+
durableStreamsUrl?: string
|
|
39
|
+
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
40
|
+
durableStreamsRouting?: DurableStreamsRoutingAdapter
|
|
41
|
+
durableStreamsServer?: DurableStreamTestServer
|
|
42
|
+
port: number
|
|
43
|
+
host?: string
|
|
44
|
+
workingDirectory?: string
|
|
45
|
+
mockStreamFn?: StreamFn
|
|
46
|
+
postgresUrl: string
|
|
47
|
+
electricUrl?: string
|
|
48
|
+
electricSecret?: string
|
|
49
|
+
authenticateRequest?: AuthenticateRequest
|
|
50
|
+
/**
|
|
51
|
+
* Disabled by default. When set to a positive interval, periodically
|
|
52
|
+
* recovers expired dispatch claims and stale outstanding wakes.
|
|
53
|
+
*/
|
|
54
|
+
dispatchRecoveryIntervalMs?: number
|
|
55
|
+
/**
|
|
56
|
+
* Age threshold for outstanding wakes recovered by the periodic loop.
|
|
57
|
+
* Defaults to dispatchRecoveryIntervalMs when periodic recovery is enabled.
|
|
58
|
+
*/
|
|
59
|
+
staleOutstandingWakeAfterMs?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface MockAgentBootstrap {
|
|
63
|
+
runtime: RuntimeHandler
|
|
64
|
+
registry: EntityRegistry
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const MOCK_CHAT_MODEL: AgentModel = {
|
|
68
|
+
id: `mock-chat`,
|
|
69
|
+
name: `Mock Chat`,
|
|
70
|
+
api: `anthropic-messages`,
|
|
71
|
+
provider: `anthropic`,
|
|
72
|
+
baseUrl: `http://mock`,
|
|
73
|
+
reasoning: false,
|
|
74
|
+
input: [`text`],
|
|
75
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
76
|
+
contextWindow: 200_000,
|
|
77
|
+
maxTokens: 4_096,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createMockAgentBootstrap(options: {
|
|
81
|
+
agentServerUrl: string
|
|
82
|
+
workingDirectory?: string
|
|
83
|
+
streamFn: StreamFn
|
|
84
|
+
}): MockAgentBootstrap {
|
|
85
|
+
const registry = createEntityRegistry()
|
|
86
|
+
|
|
87
|
+
registry.define(`chat`, {
|
|
88
|
+
description: `Mock chat entity for conformance and end-to-end tests.`,
|
|
89
|
+
handler: async (ctx) => {
|
|
90
|
+
ctx.useAgent({
|
|
91
|
+
systemPrompt: `You are a concise test assistant.`,
|
|
92
|
+
model: MOCK_CHAT_MODEL,
|
|
93
|
+
tools: [],
|
|
94
|
+
streamFn: options.streamFn,
|
|
95
|
+
})
|
|
96
|
+
await ctx.agent.run()
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const runtime = createRuntimeHandler({
|
|
101
|
+
baseUrl: options.agentServerUrl,
|
|
102
|
+
serveEndpoint: appendPathToUrl(
|
|
103
|
+
options.agentServerUrl,
|
|
104
|
+
MOCK_AGENT_HANDLER_PATH
|
|
105
|
+
),
|
|
106
|
+
registry,
|
|
107
|
+
subscriptionPathForType: (name) => `/${name}/*/main`,
|
|
108
|
+
idleTimeout: 100,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return { runtime, registry }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class ElectricAgentsServer {
|
|
115
|
+
private server?: Server
|
|
116
|
+
private electricAgentsManager?: StartedStandaloneAgentsRuntime[`manager`]
|
|
117
|
+
private pgDb?: DrizzleDB
|
|
118
|
+
private pgClient?: PgClient
|
|
119
|
+
private entityBridgeManager?: EntityBridgeCoordinator
|
|
120
|
+
private mockAgentBootstrap?: MockAgentBootstrap
|
|
121
|
+
private _url?: string
|
|
122
|
+
private shuttingDown = false
|
|
123
|
+
private streamsAgent?: Agent
|
|
124
|
+
private standaloneRuntime?: StartedStandaloneAgentsRuntime
|
|
125
|
+
|
|
126
|
+
streamClient: StreamClient
|
|
127
|
+
readonly options: ElectricAgentsServerOptions
|
|
128
|
+
|
|
129
|
+
constructor(options: ElectricAgentsServerOptions) {
|
|
130
|
+
if (!options.durableStreamsUrl && !options.durableStreamsServer) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Either durableStreamsUrl or durableStreamsServer is required`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
this.options = options
|
|
136
|
+
this.streamClient = options.durableStreamsUrl
|
|
137
|
+
? new StreamClient(
|
|
138
|
+
durableStreamsServiceUrl(options.durableStreamsUrl, this.tenantId),
|
|
139
|
+
{ bearer: options.durableStreamsBearer }
|
|
140
|
+
)
|
|
141
|
+
: null!
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get url(): string {
|
|
145
|
+
if (!this._url) {
|
|
146
|
+
throw new Error(`Server not started`)
|
|
147
|
+
}
|
|
148
|
+
return this._url
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private get publicUrl(): string {
|
|
152
|
+
if (!this._url) {
|
|
153
|
+
throw new Error(`Server not started`)
|
|
154
|
+
}
|
|
155
|
+
return this.options.baseUrl?.replace(/\/+$/, ``) ?? this._url
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private get tenantId(): string {
|
|
159
|
+
return this.options.service ?? this.options.tenantId ?? DEFAULT_TENANT_ID
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async start(): Promise<string> {
|
|
163
|
+
if (this.server) {
|
|
164
|
+
throw new Error(`Server already started`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (
|
|
169
|
+
this.options.durableStreamsServer &&
|
|
170
|
+
!this.options.durableStreamsUrl
|
|
171
|
+
) {
|
|
172
|
+
serverLog.info(`[agent-server] starting durable streams server...`)
|
|
173
|
+
const streamsUrl = await this.options.durableStreamsServer.start()
|
|
174
|
+
serverLog.info(
|
|
175
|
+
`[agent-server] durable streams server started at ${streamsUrl}`
|
|
176
|
+
)
|
|
177
|
+
this.options.durableStreamsUrl = streamsUrl
|
|
178
|
+
this.streamClient = new StreamClient(
|
|
179
|
+
durableStreamsServiceUrl(streamsUrl, this.tenantId),
|
|
180
|
+
{ bearer: this.options.durableStreamsBearer }
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.streamsAgent = new Agent({
|
|
185
|
+
keepAliveTimeout: 60_000,
|
|
186
|
+
keepAliveMaxTimeout: 600_000,
|
|
187
|
+
connections: 256,
|
|
188
|
+
pipelining: 1,
|
|
189
|
+
bodyTimeout: 0,
|
|
190
|
+
headersTimeout: 0,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
serverLog.info(`[agent-server] running migrations...`)
|
|
194
|
+
await runMigrations(this.options.postgresUrl)
|
|
195
|
+
serverLog.info(`[agent-server] migrations complete`)
|
|
196
|
+
const { db, client } = createDb(this.options.postgresUrl)
|
|
197
|
+
this.pgDb = db
|
|
198
|
+
this.pgClient = client
|
|
199
|
+
|
|
200
|
+
this.standaloneRuntime = await startStandaloneAgentsRuntime({
|
|
201
|
+
service: this.tenantId,
|
|
202
|
+
db,
|
|
203
|
+
pgClient: client,
|
|
204
|
+
streamClient: this.streamClient,
|
|
205
|
+
electricUrl: this.options.electricUrl,
|
|
206
|
+
electricSecret: this.options.electricSecret,
|
|
207
|
+
})
|
|
208
|
+
this.electricAgentsManager = this.standaloneRuntime.manager
|
|
209
|
+
this.entityBridgeManager = this.standaloneRuntime.entityBridgeManager
|
|
210
|
+
|
|
211
|
+
const serverAdapter = createServerAdapter((request) =>
|
|
212
|
+
this.handleRequest(request)
|
|
213
|
+
)
|
|
214
|
+
const server = createServer(serverAdapter)
|
|
215
|
+
this.server = server
|
|
216
|
+
|
|
217
|
+
const host = this.options.host ?? `127.0.0.1`
|
|
218
|
+
await this.listen(server, host)
|
|
219
|
+
|
|
220
|
+
if (this.options.mockStreamFn) {
|
|
221
|
+
this.mockAgentBootstrap = createMockAgentBootstrap({
|
|
222
|
+
agentServerUrl: this.publicUrl,
|
|
223
|
+
workingDirectory: this.options.workingDirectory,
|
|
224
|
+
streamFn: this.options.mockStreamFn,
|
|
225
|
+
})
|
|
226
|
+
await this.mockAgentBootstrap.runtime.registerTypes()
|
|
227
|
+
serverLog.info(
|
|
228
|
+
`[agent-server] mock chat agent registered at ${MOCK_AGENT_HANDLER_PATH}`
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return this.url
|
|
233
|
+
} catch (err) {
|
|
234
|
+
await this.stop().catch(() => {})
|
|
235
|
+
throw err
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async stop(): Promise<void> {
|
|
240
|
+
this.shuttingDown = true
|
|
241
|
+
|
|
242
|
+
if (this.server) {
|
|
243
|
+
const server = this.server
|
|
244
|
+
await new Promise<void>((resolve) => {
|
|
245
|
+
server.close(() => resolve())
|
|
246
|
+
server.closeIdleConnections?.()
|
|
247
|
+
server.closeAllConnections?.()
|
|
248
|
+
})
|
|
249
|
+
this.server = undefined
|
|
250
|
+
this._url = undefined
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.mockAgentBootstrap) {
|
|
254
|
+
this.mockAgentBootstrap.runtime.abortWakes()
|
|
255
|
+
await Promise.race([
|
|
256
|
+
this.mockAgentBootstrap.runtime.drainWakes().catch(() => {}),
|
|
257
|
+
new Promise<void>((resolve) => setTimeout(resolve, 5_000)),
|
|
258
|
+
])
|
|
259
|
+
this.mockAgentBootstrap = undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.standaloneRuntime) {
|
|
263
|
+
await this.standaloneRuntime.stop()
|
|
264
|
+
this.standaloneRuntime = undefined
|
|
265
|
+
this.entityBridgeManager = undefined
|
|
266
|
+
this.electricAgentsManager = undefined
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (this.options.durableStreamsServer) {
|
|
270
|
+
await this.options.durableStreamsServer.stop()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (this.pgClient) {
|
|
274
|
+
await this.pgClient.end()
|
|
275
|
+
this.pgClient = undefined
|
|
276
|
+
this.pgDb = undefined
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (this.streamsAgent) {
|
|
280
|
+
await this.streamsAgent.close().catch(() => {})
|
|
281
|
+
this.streamsAgent = undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.shuttingDown = false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async listen(server: Server, host: string): Promise<void> {
|
|
288
|
+
await new Promise<void>((resolve, reject) => {
|
|
289
|
+
const onError = (err: Error): void => {
|
|
290
|
+
server.off?.(`listening`, onListening)
|
|
291
|
+
reject(err)
|
|
292
|
+
}
|
|
293
|
+
const onListening = (): void => {
|
|
294
|
+
server.off?.(`error`, onError)
|
|
295
|
+
resolve()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
server.once?.(`error`, onError) ?? server.on(`error`, onError)
|
|
299
|
+
server.listen(this.options.port, host, onListening)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const addr = server.address()
|
|
303
|
+
if (typeof addr === `string`) {
|
|
304
|
+
this._url = addr
|
|
305
|
+
} else if (addr) {
|
|
306
|
+
const resolvedHost = host === `0.0.0.0` ? `127.0.0.1` : host
|
|
307
|
+
this._url = `http://${resolvedHost}:${addr.port}`
|
|
308
|
+
} else {
|
|
309
|
+
throw new Error(`Could not determine server address`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async handleRequest(request: Request): Promise<Response> {
|
|
314
|
+
if (
|
|
315
|
+
!this._url ||
|
|
316
|
+
!this.standaloneRuntime ||
|
|
317
|
+
!this.electricAgentsManager ||
|
|
318
|
+
!this.entityBridgeManager ||
|
|
319
|
+
!this.pgDb ||
|
|
320
|
+
!this.streamsAgent ||
|
|
321
|
+
!this.options.durableStreamsUrl
|
|
322
|
+
) {
|
|
323
|
+
return new Response(null, { status: 503 })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return await ossServerRouter.fetch(
|
|
327
|
+
request as Parameters<typeof ossServerRouter.fetch>[0],
|
|
328
|
+
await this.buildTenantContext(request)
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private async buildTenantContext(
|
|
333
|
+
request: Request
|
|
334
|
+
): Promise<OssServerContext> {
|
|
335
|
+
if (
|
|
336
|
+
!this.standaloneRuntime ||
|
|
337
|
+
!this.electricAgentsManager ||
|
|
338
|
+
!this.entityBridgeManager ||
|
|
339
|
+
!this.pgDb ||
|
|
340
|
+
!this.streamsAgent ||
|
|
341
|
+
!this.options.durableStreamsUrl
|
|
342
|
+
) {
|
|
343
|
+
throw new Error(`agents-server runtime is not started`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
service: this.tenantId,
|
|
348
|
+
authenticatedUser:
|
|
349
|
+
(await this.options.authenticateRequest?.(request)) ?? undefined,
|
|
350
|
+
publicUrl: this.publicUrl,
|
|
351
|
+
localUrl: this._url,
|
|
352
|
+
durableStreamsUrl: this.options.durableStreamsUrl,
|
|
353
|
+
durableStreamsBearer: this.options.durableStreamsBearer,
|
|
354
|
+
durableStreamsRouting:
|
|
355
|
+
this.options.durableStreamsRouting ??
|
|
356
|
+
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
357
|
+
durableStreamsDispatcher: this.streamsAgent,
|
|
358
|
+
electricUrl: this.options.electricUrl,
|
|
359
|
+
electricSecret: this.options.electricSecret,
|
|
360
|
+
ownAgentHandlerPaths: this.mockAgentBootstrap
|
|
361
|
+
? [MOCK_AGENT_HANDLER_PATH]
|
|
362
|
+
: undefined,
|
|
363
|
+
pgDb: this.pgDb,
|
|
364
|
+
entityManager: this.electricAgentsManager,
|
|
365
|
+
streamClient: this.streamClient,
|
|
366
|
+
runtime: this.standaloneRuntime.runtime,
|
|
367
|
+
entityBridgeManager: this.entityBridgeManager,
|
|
368
|
+
isShuttingDown: () => this.shuttingDown,
|
|
369
|
+
mockAgent: this.mockAgentBootstrap
|
|
370
|
+
? { runtime: this.mockAgentBootstrap.runtime }
|
|
371
|
+
: undefined,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { PostgresRegistry } from './entity-registry.js'
|
|
3
|
+
import { EntityBridgeManager } from './entity-bridge-manager.js'
|
|
4
|
+
import { serverLog } from './utils/log.js'
|
|
5
|
+
import { ElectricAgentsTenantRuntime } from './runtime.js'
|
|
6
|
+
import { Scheduler } from './scheduler.js'
|
|
7
|
+
import { StreamClient, durableStreamsServiceUrl } from './stream-client.js'
|
|
8
|
+
import { TagStreamOutboxDrainer } from './tag-stream-outbox-drainer.js'
|
|
9
|
+
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
10
|
+
import { WakeRegistry } from './wake-registry.js'
|
|
11
|
+
import type { DrizzleDB, PgClient } from './db/index.js'
|
|
12
|
+
import type { EntityManager } from './entity-manager.js'
|
|
13
|
+
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
14
|
+
import type { CronTickPayload, DelayedSendPayload } from './scheduler.js'
|
|
15
|
+
import type { DurableStreamsBearerProvider } from './stream-client.js'
|
|
16
|
+
|
|
17
|
+
export interface StandaloneAgentsRuntimeOptions {
|
|
18
|
+
service?: string
|
|
19
|
+
tenantId?: string
|
|
20
|
+
db: DrizzleDB
|
|
21
|
+
pgClient: PgClient
|
|
22
|
+
durableStreamsUrl?: string
|
|
23
|
+
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
24
|
+
streamClient?: StreamClient
|
|
25
|
+
electricUrl?: string
|
|
26
|
+
electricSecret?: string
|
|
27
|
+
instanceId?: string
|
|
28
|
+
wakeRegistry?: WakeRegistry
|
|
29
|
+
startWakeRegistry?: boolean
|
|
30
|
+
startScheduler?: boolean
|
|
31
|
+
startTagStreamOutboxDrainer?: boolean
|
|
32
|
+
startEntityBridgeManager?: boolean
|
|
33
|
+
rehydrateOnStart?: boolean
|
|
34
|
+
entityBridgeManager?: EntityBridgeCoordinator
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StartedStandaloneAgentsRuntime {
|
|
38
|
+
serviceId: string
|
|
39
|
+
service: string
|
|
40
|
+
db: DrizzleDB
|
|
41
|
+
pgClient: PgClient
|
|
42
|
+
streamClient: StreamClient
|
|
43
|
+
registry: PostgresRegistry
|
|
44
|
+
wakeRegistry: WakeRegistry
|
|
45
|
+
runtime: ElectricAgentsTenantRuntime
|
|
46
|
+
manager: EntityManager
|
|
47
|
+
scheduler: Scheduler
|
|
48
|
+
entityBridgeManager: EntityBridgeCoordinator
|
|
49
|
+
tagStreamOutboxDrainer: TagStreamOutboxDrainer
|
|
50
|
+
stop: () => Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function startStandaloneAgentsRuntime(
|
|
54
|
+
options: StandaloneAgentsRuntimeOptions
|
|
55
|
+
): Promise<StartedStandaloneAgentsRuntime> {
|
|
56
|
+
const serviceId = options.service ?? options.tenantId ?? DEFAULT_TENANT_ID
|
|
57
|
+
const streamClient =
|
|
58
|
+
options.streamClient ??
|
|
59
|
+
(options.durableStreamsUrl
|
|
60
|
+
? new StreamClient(
|
|
61
|
+
durableStreamsServiceUrl(options.durableStreamsUrl, serviceId),
|
|
62
|
+
{ bearer: options.durableStreamsBearer }
|
|
63
|
+
)
|
|
64
|
+
: undefined)
|
|
65
|
+
if (!streamClient) {
|
|
66
|
+
throw new Error(`Either durableStreamsUrl or streamClient is required`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const registry = new PostgresRegistry(options.db, serviceId)
|
|
70
|
+
const wakeRegistry =
|
|
71
|
+
options.wakeRegistry ?? new WakeRegistry(options.db, serviceId)
|
|
72
|
+
let runtime: ElectricAgentsTenantRuntime
|
|
73
|
+
const scheduler = new Scheduler({
|
|
74
|
+
pgClient: options.pgClient,
|
|
75
|
+
instanceId: options.instanceId ?? randomUUID(),
|
|
76
|
+
tenantId: serviceId,
|
|
77
|
+
executors: {
|
|
78
|
+
delayed_send: async (payload: DelayedSendPayload, taskId: number) => {
|
|
79
|
+
await runtime.executeDelayedSend(payload, taskId)
|
|
80
|
+
},
|
|
81
|
+
cron_tick: async (payload: CronTickPayload, tickNumber: number) => {
|
|
82
|
+
await runtime.executeCronTick(payload, tickNumber)
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
const entityBridgeManager =
|
|
87
|
+
options.entityBridgeManager ??
|
|
88
|
+
new EntityBridgeManager(
|
|
89
|
+
registry,
|
|
90
|
+
streamClient,
|
|
91
|
+
options.electricUrl,
|
|
92
|
+
options.electricSecret,
|
|
93
|
+
serviceId
|
|
94
|
+
)
|
|
95
|
+
const tagStreamOutboxDrainer = new TagStreamOutboxDrainer(
|
|
96
|
+
registry,
|
|
97
|
+
streamClient
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
runtime = new ElectricAgentsTenantRuntime({
|
|
101
|
+
service: serviceId,
|
|
102
|
+
db: options.db,
|
|
103
|
+
registry,
|
|
104
|
+
streamClient,
|
|
105
|
+
wakeRegistry,
|
|
106
|
+
scheduler,
|
|
107
|
+
entityBridgeManager,
|
|
108
|
+
stopWakeRegistryOnShutdown: options.wakeRegistry ? false : true,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const startWakeRegistry = options.startWakeRegistry ?? true
|
|
112
|
+
const startScheduler = options.startScheduler ?? true
|
|
113
|
+
const startTagStreamOutboxDrainer =
|
|
114
|
+
options.startTagStreamOutboxDrainer ?? true
|
|
115
|
+
const startEntityBridgeManager = options.startEntityBridgeManager ?? true
|
|
116
|
+
const rehydrateOnStart = options.rehydrateOnStart ?? true
|
|
117
|
+
let entityBridgeManagerStarted = false
|
|
118
|
+
let tagStreamOutboxDrainerStarted = false
|
|
119
|
+
let schedulerStarted = false
|
|
120
|
+
let stopped = false
|
|
121
|
+
|
|
122
|
+
const stop = async (): Promise<void> => {
|
|
123
|
+
if (stopped) return
|
|
124
|
+
stopped = true
|
|
125
|
+
if (schedulerStarted) {
|
|
126
|
+
await scheduler.stop()
|
|
127
|
+
schedulerStarted = false
|
|
128
|
+
}
|
|
129
|
+
if (tagStreamOutboxDrainerStarted) {
|
|
130
|
+
await tagStreamOutboxDrainer.stop()
|
|
131
|
+
tagStreamOutboxDrainerStarted = false
|
|
132
|
+
}
|
|
133
|
+
if (entityBridgeManagerStarted) {
|
|
134
|
+
await entityBridgeManager.stop()
|
|
135
|
+
entityBridgeManagerStarted = false
|
|
136
|
+
}
|
|
137
|
+
await runtime.stop()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (startWakeRegistry) {
|
|
142
|
+
serverLog.info(`[agent-server] rebuilding wake registry...`)
|
|
143
|
+
await runtime.manager.rebuildWakeRegistry(
|
|
144
|
+
options.electricUrl,
|
|
145
|
+
options.electricSecret
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
if (rehydrateOnStart) {
|
|
149
|
+
serverLog.info(`[agent-server] rehydrating cron schedules...`)
|
|
150
|
+
await runtime.rehydrateCronSchedules()
|
|
151
|
+
}
|
|
152
|
+
if (startEntityBridgeManager) {
|
|
153
|
+
serverLog.info(`[agent-server] starting entity bridge manager...`)
|
|
154
|
+
await entityBridgeManager.start()
|
|
155
|
+
entityBridgeManagerStarted = true
|
|
156
|
+
}
|
|
157
|
+
if (startTagStreamOutboxDrainer) {
|
|
158
|
+
serverLog.info(`[agent-server] starting tag stream outbox drainer...`)
|
|
159
|
+
tagStreamOutboxDrainer.start()
|
|
160
|
+
tagStreamOutboxDrainerStarted = true
|
|
161
|
+
}
|
|
162
|
+
if (startScheduler) {
|
|
163
|
+
serverLog.info(`[agent-server] starting scheduler...`)
|
|
164
|
+
schedulerStarted = true
|
|
165
|
+
await scheduler.start()
|
|
166
|
+
serverLog.info(`[agent-server] scheduler started`)
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await stop().catch(() => {})
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
serviceId,
|
|
175
|
+
service: serviceId,
|
|
176
|
+
db: options.db,
|
|
177
|
+
pgClient: options.pgClient,
|
|
178
|
+
streamClient,
|
|
179
|
+
registry,
|
|
180
|
+
wakeRegistry,
|
|
181
|
+
runtime,
|
|
182
|
+
manager: runtime.manager,
|
|
183
|
+
scheduler,
|
|
184
|
+
entityBridgeManager,
|
|
185
|
+
tagStreamOutboxDrainer,
|
|
186
|
+
stop,
|
|
187
|
+
}
|
|
188
|
+
}
|