@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
@@ -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
+ }