@electric-ax/agents-server 0.4.6 → 0.4.9
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/dist/entrypoint.js +314 -38
- package/dist/index.cjs +332 -37
- package/dist/index.d.cts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.js +328 -39
- package/drizzle/0009_entity_signal_statuses.sql +3 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/db/schema.ts +1 -1
- package/src/electric-agents-types.ts +77 -1
- package/src/entity-manager.ts +320 -33
- package/src/entity-registry.ts +40 -16
- package/src/index.ts +29 -1
- package/src/manifest-side-effects.ts +11 -0
- package/src/routing/context.ts +18 -1
- package/src/routing/dispatch-policy.ts +6 -0
- package/src/routing/entities-router.ts +187 -1
- package/src/routing/internal-router.ts +25 -4
- package/src/routing/runners-router.ts +1 -1
- package/src/runtime.ts +5 -1
- package/src/server.ts +12 -0
package/src/entity-registry.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
assertEntityStatus,
|
|
16
16
|
assertRunnerAdminStatus,
|
|
17
17
|
assertRunnerKind,
|
|
18
|
+
isTerminalEntityStatus,
|
|
18
19
|
} from './electric-agents-types.js'
|
|
19
20
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
20
21
|
import type { DrizzleDB } from './db/index.js'
|
|
@@ -726,10 +727,13 @@ export class PostgresRegistry {
|
|
|
726
727
|
}
|
|
727
728
|
|
|
728
729
|
async updateStatus(entityUrl: string, status: EntityStatus): Promise<void> {
|
|
729
|
-
const whereClause =
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
730
|
+
const whereClause = isTerminalEntityStatus(status)
|
|
731
|
+
? this.entityWhere(entityUrl)
|
|
732
|
+
: and(
|
|
733
|
+
this.entityWhere(entityUrl),
|
|
734
|
+
ne(entities.status, `stopped`),
|
|
735
|
+
ne(entities.status, `killed`)
|
|
736
|
+
)
|
|
733
737
|
|
|
734
738
|
await this.db
|
|
735
739
|
.update(entities)
|
|
@@ -740,21 +744,41 @@ export class PostgresRegistry {
|
|
|
740
744
|
async updateStatusWithTxid(
|
|
741
745
|
entityUrl: string,
|
|
742
746
|
status: EntityStatus
|
|
743
|
-
): Promise<number> {
|
|
747
|
+
): Promise<number | null> {
|
|
744
748
|
return await this.db.transaction(async (tx) => {
|
|
745
|
-
const
|
|
746
|
-
status === `stopped`
|
|
747
|
-
? this.entityWhere(entityUrl)
|
|
748
|
-
: and(this.entityWhere(entityUrl), ne(entities.status, `stopped`))
|
|
749
|
-
|
|
750
|
-
await tx
|
|
749
|
+
const rows = await tx
|
|
751
750
|
.update(entities)
|
|
752
751
|
.set({ status, updatedAt: Date.now() })
|
|
753
|
-
.where(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
752
|
+
.where(
|
|
753
|
+
and(
|
|
754
|
+
this.entityWhere(entityUrl),
|
|
755
|
+
ne(entities.status, `stopped`),
|
|
756
|
+
ne(entities.status, `killed`)
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
.returning({
|
|
760
|
+
txid: sql<string>`pg_current_xact_id()::xid::text`,
|
|
761
|
+
})
|
|
762
|
+
return rows[0] ? parseInt(rows[0].txid) : null
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async touchEntityWithTxid(entityUrl: string): Promise<number | null> {
|
|
767
|
+
return await this.db.transaction(async (tx) => {
|
|
768
|
+
const rows = await tx
|
|
769
|
+
.update(entities)
|
|
770
|
+
.set({ updatedAt: Date.now() })
|
|
771
|
+
.where(
|
|
772
|
+
and(
|
|
773
|
+
eq(entities.url, entityUrl),
|
|
774
|
+
ne(entities.status, `stopped`),
|
|
775
|
+
ne(entities.status, `killed`)
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
.returning({
|
|
779
|
+
txid: sql<string>`pg_current_xact_id()::xid::text`,
|
|
780
|
+
})
|
|
781
|
+
return rows[0] ? parseInt(rows[0].txid) : null
|
|
758
782
|
})
|
|
759
783
|
}
|
|
760
784
|
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,14 @@ export type {
|
|
|
15
15
|
SubscriptionResponse,
|
|
16
16
|
SubscriptionStreamInfo,
|
|
17
17
|
} from './stream-client.js'
|
|
18
|
+
export {
|
|
19
|
+
assertEntitySignal,
|
|
20
|
+
assertEntityStatus,
|
|
21
|
+
expectedSignalStatus,
|
|
22
|
+
isTerminalEntityStatus,
|
|
23
|
+
rejectsNormalWrites,
|
|
24
|
+
toPublicEntity,
|
|
25
|
+
} from './electric-agents-types.js'
|
|
18
26
|
export type {
|
|
19
27
|
AuthenticateRequest,
|
|
20
28
|
ConsumerClaim,
|
|
@@ -32,11 +40,31 @@ export type {
|
|
|
32
40
|
RunnerLiveness,
|
|
33
41
|
SourceStreamOffset,
|
|
34
42
|
WakeNotificationRow,
|
|
43
|
+
ElectricAgentsEntity,
|
|
44
|
+
ElectricAgentsEntityRow,
|
|
45
|
+
ElectricAgentsEntityType,
|
|
46
|
+
EntityStatus,
|
|
47
|
+
EntitySignal,
|
|
48
|
+
PublicElectricAgentsEntity,
|
|
49
|
+
EntityListFilter,
|
|
50
|
+
RegisterEntityTypeRequest,
|
|
51
|
+
SendRequest,
|
|
52
|
+
SignalRequest,
|
|
53
|
+
SignalResponse,
|
|
54
|
+
TypedSpawnRequest,
|
|
35
55
|
} from './electric-agents-types.js'
|
|
56
|
+
export type {
|
|
57
|
+
EventSourceBucket,
|
|
58
|
+
EventSourceContract,
|
|
59
|
+
EventSourceFilter,
|
|
60
|
+
EventSourceSubscription,
|
|
61
|
+
EventSourceSubscriptionInput,
|
|
62
|
+
SubscriptionLifetime,
|
|
63
|
+
} from '@electric-ax/agents-runtime'
|
|
36
64
|
export type { Principal, PrincipalKind } from './principal.js'
|
|
37
65
|
export { globalRouter } from './routing/global-router.js'
|
|
38
66
|
export type { GlobalRoutes } from './routing/global-router.js'
|
|
39
|
-
export type { TenantContext } from './routing/context.js'
|
|
67
|
+
export type { EventSourceCatalog, TenantContext } from './routing/context.js'
|
|
40
68
|
export {
|
|
41
69
|
streamRootDurableStreamsRoutingAdapter,
|
|
42
70
|
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCronStreamPathFromSpec,
|
|
3
|
+
getWebhookStreamPath,
|
|
3
4
|
getSharedStateStreamPath,
|
|
4
5
|
resolveCronScheduleSpec,
|
|
5
6
|
} from '@electric-ax/agents-runtime'
|
|
@@ -56,6 +57,16 @@ export function extractManifestSourceUrl(
|
|
|
56
57
|
: undefined
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
if (manifest.sourceType === `webhook`) {
|
|
61
|
+
if (typeof config?.streamUrl === `string`) return config.streamUrl
|
|
62
|
+
if (typeof config?.endpointKey === `string`) {
|
|
63
|
+
return getWebhookStreamPath(
|
|
64
|
+
config.endpointKey,
|
|
65
|
+
typeof config.bucket === `string` ? config.bucket : undefined
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
return undefined
|
|
60
71
|
}
|
|
61
72
|
|
package/src/routing/context.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Agent } from 'undici'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
EventSourceContract,
|
|
4
|
+
WebhookSignatureVerifierConfig,
|
|
5
|
+
} from '@electric-ax/agents-runtime'
|
|
3
6
|
import type { DrizzleDB } from '../db/index.js'
|
|
4
7
|
import type { EntityBridgeCoordinator } from '../entity-bridge-manager.js'
|
|
5
8
|
import type { EntityManager } from '../entity-manager.js'
|
|
@@ -10,6 +13,18 @@ import type { Principal } from '../principal.js'
|
|
|
10
13
|
import type { DurableStreamsBearerProvider } from '../stream-client.js'
|
|
11
14
|
import type { WebhookSigner } from '../webhook-signing.js'
|
|
12
15
|
|
|
16
|
+
export interface EventSourceCatalog {
|
|
17
|
+
listEventSources: () =>
|
|
18
|
+
| Array<EventSourceContract>
|
|
19
|
+
| Promise<Array<EventSourceContract>>
|
|
20
|
+
getEventSource: (
|
|
21
|
+
sourceKey: string
|
|
22
|
+
) =>
|
|
23
|
+
| EventSourceContract
|
|
24
|
+
| undefined
|
|
25
|
+
| Promise<EventSourceContract | undefined>
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
/**
|
|
14
29
|
* Per-request tenant context passed through every router and handler.
|
|
15
30
|
*
|
|
@@ -38,5 +53,7 @@ export interface TenantContext {
|
|
|
38
53
|
streamClient: StreamClient
|
|
39
54
|
runtime: ElectricAgentsTenantRuntime
|
|
40
55
|
entityBridgeManager: EntityBridgeCoordinator
|
|
56
|
+
eventSources?: EventSourceCatalog
|
|
57
|
+
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
|
|
41
58
|
isShuttingDown: () => boolean
|
|
42
59
|
}
|
|
@@ -217,6 +217,12 @@ export async function assertDispatchPolicyAllowed(
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
export function shouldLinkDispatchBeforeInitialMessage(
|
|
221
|
+
policy: DispatchPolicy | undefined
|
|
222
|
+
): boolean {
|
|
223
|
+
return policy?.targets[0] !== undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
220
226
|
export async function linkEntityDispatchSubscription(
|
|
221
227
|
ctx: TenantContext,
|
|
222
228
|
entity: ElectricAgentsEntity
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Type, type Static } from '@sinclair/typebox'
|
|
6
|
+
import {
|
|
7
|
+
buildEventSourceManifestEntry,
|
|
8
|
+
resolveEventSourceSubscription,
|
|
9
|
+
} from '@electric-ax/agents-runtime'
|
|
6
10
|
import { Router, json, status } from 'itty-router'
|
|
7
11
|
import { apiError } from '../electric-agents-http.js'
|
|
8
12
|
import { parsePrincipalKey, principalUrl } from '../principal.js'
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
backfillEntityDispatchPolicy,
|
|
19
23
|
linkEntityDispatchSubscription,
|
|
20
24
|
resolveEffectiveDispatchPolicyForSpawn,
|
|
25
|
+
shouldLinkDispatchBeforeInitialMessage,
|
|
21
26
|
unlinkEntityDispatchSubscription,
|
|
22
27
|
} from './dispatch-policy.js'
|
|
23
28
|
import { routeBody, withSchema } from './schema.js'
|
|
@@ -25,6 +30,7 @@ import type { ElectricAgentsEntity } from '../electric-agents-types.js'
|
|
|
25
30
|
import type { JsonRouteRequest } from './schema.js'
|
|
26
31
|
import type { RouterType } from 'itty-router'
|
|
27
32
|
import type { TenantContext } from './context.js'
|
|
33
|
+
import type { EventSourceSubscriptionInput } from '@electric-ax/agents-runtime'
|
|
28
34
|
|
|
29
35
|
interface AgentsRouteRequest extends JsonRouteRequest {
|
|
30
36
|
entityRoute?: ExistingEntityRoute
|
|
@@ -83,6 +89,7 @@ const spawnBodySchema = Type.Object({
|
|
|
83
89
|
debounceMs: Type.Optional(Type.Number()),
|
|
84
90
|
timeoutMs: Type.Optional(Type.Number()),
|
|
85
91
|
includeResponse: Type.Optional(Type.Boolean()),
|
|
92
|
+
manifestKey: Type.Optional(Type.String()),
|
|
86
93
|
})
|
|
87
94
|
),
|
|
88
95
|
})
|
|
@@ -133,6 +140,22 @@ const setTagBodySchema = Type.Object({
|
|
|
133
140
|
value: Type.String(),
|
|
134
141
|
})
|
|
135
142
|
|
|
143
|
+
const entitySignalSchema = Type.Union([
|
|
144
|
+
Type.Literal(`SIGINT`),
|
|
145
|
+
Type.Literal(`SIGHUP`),
|
|
146
|
+
Type.Literal(`SIGTERM`),
|
|
147
|
+
Type.Literal(`SIGKILL`),
|
|
148
|
+
Type.Literal(`SIGSTOP`),
|
|
149
|
+
Type.Literal(`SIGCONT`),
|
|
150
|
+
Type.Literal(`SIGUSR`),
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
const signalBodySchema = Type.Object({
|
|
154
|
+
signal: entitySignalSchema,
|
|
155
|
+
reason: Type.Optional(Type.String()),
|
|
156
|
+
payload: Type.Optional(Type.Unknown()),
|
|
157
|
+
})
|
|
158
|
+
|
|
136
159
|
const scheduleBodySchema = Type.Union([
|
|
137
160
|
Type.Object({
|
|
138
161
|
scheduleType: Type.Literal(`cron`),
|
|
@@ -152,6 +175,24 @@ const scheduleBodySchema = Type.Union([
|
|
|
152
175
|
}),
|
|
153
176
|
])
|
|
154
177
|
|
|
178
|
+
const subscriptionLifetimeSchema = Type.Union([
|
|
179
|
+
Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
|
|
180
|
+
Type.Object({
|
|
181
|
+
kind: Type.Literal(`expires_at`),
|
|
182
|
+
at: Type.String(),
|
|
183
|
+
}),
|
|
184
|
+
Type.Object({ kind: Type.Literal(`manual`) }),
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
const eventSourceSubscriptionBodySchema = Type.Object({
|
|
188
|
+
sourceKey: Type.String(),
|
|
189
|
+
bucketKey: Type.Optional(Type.String()),
|
|
190
|
+
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
191
|
+
filterKey: Type.Optional(Type.String()),
|
|
192
|
+
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
193
|
+
reason: Type.Optional(Type.String()),
|
|
194
|
+
})
|
|
195
|
+
|
|
155
196
|
const entitiesRegisterBodySchema = Type.Object({
|
|
156
197
|
tags: Type.Optional(stringRecordSchema),
|
|
157
198
|
})
|
|
@@ -161,7 +202,11 @@ type SendBody = Static<typeof sendBodySchema>
|
|
|
161
202
|
type InboxMessageBody = Static<typeof inboxMessageBodySchema>
|
|
162
203
|
type ForkBody = Static<typeof forkBodySchema>
|
|
163
204
|
type SetTagBody = Static<typeof setTagBodySchema>
|
|
205
|
+
type SignalBody = Static<typeof signalBodySchema>
|
|
164
206
|
type ScheduleBody = Static<typeof scheduleBodySchema>
|
|
207
|
+
type EventSourceSubscriptionBody = Static<
|
|
208
|
+
typeof eventSourceSubscriptionBodySchema
|
|
209
|
+
>
|
|
165
210
|
type EntitiesRegisterBody = Static<typeof entitiesRegisterBodySchema>
|
|
166
211
|
|
|
167
212
|
export const entitiesRouter: EntitiesRoutes = Router<
|
|
@@ -187,6 +232,12 @@ entitiesRouter.put(
|
|
|
187
232
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity)
|
|
188
233
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity)
|
|
189
234
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity)
|
|
235
|
+
entitiesRouter.post(
|
|
236
|
+
`/:type/:instanceId/signal`,
|
|
237
|
+
withExistingEntity,
|
|
238
|
+
withSchema(signalBodySchema),
|
|
239
|
+
signalEntity
|
|
240
|
+
)
|
|
190
241
|
entitiesRouter.post(
|
|
191
242
|
`/:type/:instanceId/send`,
|
|
192
243
|
withExistingEntity,
|
|
@@ -232,6 +283,17 @@ entitiesRouter.delete(
|
|
|
232
283
|
withExistingEntity,
|
|
233
284
|
deleteSchedule
|
|
234
285
|
)
|
|
286
|
+
entitiesRouter.put(
|
|
287
|
+
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
288
|
+
withExistingEntity,
|
|
289
|
+
withSchema(eventSourceSubscriptionBodySchema),
|
|
290
|
+
upsertEventSourceSubscription
|
|
291
|
+
)
|
|
292
|
+
entitiesRouter.delete(
|
|
293
|
+
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
294
|
+
withExistingEntity,
|
|
295
|
+
deleteEventSourceSubscription
|
|
296
|
+
)
|
|
235
297
|
|
|
236
298
|
function entityUrlFromSegments(
|
|
237
299
|
type: string,
|
|
@@ -443,6 +505,98 @@ async function deleteSchedule(
|
|
|
443
505
|
return json(result)
|
|
444
506
|
}
|
|
445
507
|
|
|
508
|
+
async function upsertEventSourceSubscription(
|
|
509
|
+
request: AgentsRouteRequest,
|
|
510
|
+
ctx: TenantContext
|
|
511
|
+
): Promise<Response> {
|
|
512
|
+
const principalMutationError = rejectPrincipalEntityMutation(
|
|
513
|
+
request,
|
|
514
|
+
`subscribed to event sources`
|
|
515
|
+
)
|
|
516
|
+
if (principalMutationError) return principalMutationError
|
|
517
|
+
|
|
518
|
+
const catalog = ctx.eventSources
|
|
519
|
+
if (!catalog) {
|
|
520
|
+
return apiError(
|
|
521
|
+
404,
|
|
522
|
+
ErrCodeNotFound,
|
|
523
|
+
`No event source catalog is configured`
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
528
|
+
const parsed = routeBody<EventSourceSubscriptionBody>(request)
|
|
529
|
+
const source = await catalog.getEventSource(parsed.sourceKey)
|
|
530
|
+
if (!source) {
|
|
531
|
+
return apiError(
|
|
532
|
+
404,
|
|
533
|
+
ErrCodeNotFound,
|
|
534
|
+
`Event source "${parsed.sourceKey}" not found`
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
539
|
+
const expiresAt = new Date(parsed.lifetime.at)
|
|
540
|
+
if (Number.isNaN(expiresAt.getTime())) {
|
|
541
|
+
return apiError(
|
|
542
|
+
400,
|
|
543
|
+
ErrCodeInvalidRequest,
|
|
544
|
+
`Invalid expires_at lifetime timestamp`
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let resolved: ReturnType<typeof resolveEventSourceSubscription>
|
|
550
|
+
try {
|
|
551
|
+
resolved = resolveEventSourceSubscription({
|
|
552
|
+
contract: source,
|
|
553
|
+
entityUrl,
|
|
554
|
+
request: {
|
|
555
|
+
...(parsed as EventSourceSubscriptionInput),
|
|
556
|
+
id: decodeURIComponent(request.params.subscriptionId),
|
|
557
|
+
},
|
|
558
|
+
createdBy: `tool`,
|
|
559
|
+
})
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return apiError(
|
|
562
|
+
400,
|
|
563
|
+
ErrCodeInvalidRequest,
|
|
564
|
+
error instanceof Error ? error.message : String(error)
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl)
|
|
569
|
+
|
|
570
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(
|
|
571
|
+
entityUrl,
|
|
572
|
+
{
|
|
573
|
+
subscription: resolved.subscription,
|
|
574
|
+
manifest: buildEventSourceManifestEntry(resolved),
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
return json(result)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function deleteEventSourceSubscription(
|
|
581
|
+
request: AgentsRouteRequest,
|
|
582
|
+
ctx: TenantContext
|
|
583
|
+
): Promise<Response> {
|
|
584
|
+
const principalMutationError = rejectPrincipalEntityMutation(
|
|
585
|
+
request,
|
|
586
|
+
`unsubscribed from event sources`
|
|
587
|
+
)
|
|
588
|
+
if (principalMutationError) return principalMutationError
|
|
589
|
+
|
|
590
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
591
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(
|
|
592
|
+
entityUrl,
|
|
593
|
+
{
|
|
594
|
+
id: decodeURIComponent(request.params.subscriptionId),
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
return json(result)
|
|
598
|
+
}
|
|
599
|
+
|
|
446
600
|
async function setTag(
|
|
447
601
|
request: AgentsRouteRequest,
|
|
448
602
|
ctx: TenantContext
|
|
@@ -614,13 +768,21 @@ async function spawnEntity(
|
|
|
614
768
|
wake: parsed.wake,
|
|
615
769
|
created_by: principal.url,
|
|
616
770
|
})
|
|
617
|
-
|
|
771
|
+
const linkBeforeInitialMessage =
|
|
772
|
+
parsed.initialMessage !== undefined &&
|
|
773
|
+
shouldLinkDispatchBeforeInitialMessage(dispatchPolicy)
|
|
774
|
+
if (linkBeforeInitialMessage) {
|
|
775
|
+
await linkEntityDispatchSubscription(ctx, entity)
|
|
776
|
+
}
|
|
618
777
|
if (parsed.initialMessage !== undefined) {
|
|
619
778
|
await ctx.entityManager.send(entity.url, {
|
|
620
779
|
from: principal.url,
|
|
621
780
|
payload: parsed.initialMessage,
|
|
622
781
|
})
|
|
623
782
|
}
|
|
783
|
+
if (!linkBeforeInitialMessage) {
|
|
784
|
+
await linkEntityDispatchSubscription(ctx, entity)
|
|
785
|
+
}
|
|
624
786
|
|
|
625
787
|
return json(
|
|
626
788
|
{ ...toPublicEntity(entity), txid: entity.txid },
|
|
@@ -655,3 +817,27 @@ async function killEntity(
|
|
|
655
817
|
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main)
|
|
656
818
|
return json(result)
|
|
657
819
|
}
|
|
820
|
+
|
|
821
|
+
async function signalEntity(
|
|
822
|
+
request: AgentsRouteRequest,
|
|
823
|
+
ctx: TenantContext
|
|
824
|
+
): Promise<Response> {
|
|
825
|
+
const principalMutationError = rejectPrincipalEntityMutation(
|
|
826
|
+
request,
|
|
827
|
+
`signaled`
|
|
828
|
+
)
|
|
829
|
+
if (principalMutationError) return principalMutationError
|
|
830
|
+
|
|
831
|
+
const parsed = routeBody<SignalBody>(request)
|
|
832
|
+
const { entityUrl, entity } = requireExistingEntityRoute(request)
|
|
833
|
+
const result = await ctx.entityManager.signal(entityUrl, {
|
|
834
|
+
signal: parsed.signal,
|
|
835
|
+
reason: parsed.reason,
|
|
836
|
+
payload: parsed.payload,
|
|
837
|
+
})
|
|
838
|
+
if (result.new_state === `stopped` || result.new_state === `killed`) {
|
|
839
|
+
await unlinkEntityDispatchSubscription(ctx, entity)
|
|
840
|
+
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main)
|
|
841
|
+
}
|
|
842
|
+
return json(result)
|
|
843
|
+
}
|
|
@@ -36,7 +36,10 @@ import { runnersRouter } from './runners-router.js'
|
|
|
36
36
|
import { routeBody, validateOptionalJsonBody, withSchema } from './schema.js'
|
|
37
37
|
import { withLeadingSlash } from './tenant-stream-paths.js'
|
|
38
38
|
import type { IRequest, RouterType } from 'itty-router'
|
|
39
|
-
import type {
|
|
39
|
+
import type {
|
|
40
|
+
EventSourceContract,
|
|
41
|
+
WebhookSignatureVerifierConfig,
|
|
42
|
+
} from '@electric-ax/agents-runtime'
|
|
40
43
|
import type { TenantContext } from './context.js'
|
|
41
44
|
import type { DurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
|
|
42
45
|
import type { WebhookSigner } from '../webhook-signing.js'
|
|
@@ -117,6 +120,7 @@ export const internalRouter: InternalRoutes = Router<
|
|
|
117
120
|
})
|
|
118
121
|
|
|
119
122
|
internalRouter.get(`/health`, () => json({ status: `ok` }))
|
|
123
|
+
internalRouter.get(`/event-sources`, listEventSources)
|
|
120
124
|
internalRouter.post(
|
|
121
125
|
`/wake`,
|
|
122
126
|
withSchema(wakeRegistrationBodySchema),
|
|
@@ -335,6 +339,20 @@ async function registerWake(
|
|
|
335
339
|
return status(204)
|
|
336
340
|
}
|
|
337
341
|
|
|
342
|
+
async function listEventSources(
|
|
343
|
+
_request: IRequest,
|
|
344
|
+
ctx: TenantContext
|
|
345
|
+
): Promise<Response> {
|
|
346
|
+
const eventSources = ctx.eventSources
|
|
347
|
+
? await ctx.eventSources.listEventSources()
|
|
348
|
+
: []
|
|
349
|
+
return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isAgentVisibleEventSource(source: EventSourceContract): boolean {
|
|
353
|
+
return source.agentVisible === true && source.status === `active`
|
|
354
|
+
}
|
|
355
|
+
|
|
338
356
|
async function webhookForward(
|
|
339
357
|
request: IRequest,
|
|
340
358
|
ctx: TenantContext
|
|
@@ -479,7 +497,7 @@ async function webhookForward(
|
|
|
479
497
|
enrichPromise,
|
|
480
498
|
])
|
|
481
499
|
|
|
482
|
-
if (entity?.status === `stopped`) {
|
|
500
|
+
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
483
501
|
if (upsertPromise) await upsertPromise
|
|
484
502
|
return json({ done: true })
|
|
485
503
|
}
|
|
@@ -764,10 +782,13 @@ async function callbackForward(
|
|
|
764
782
|
// the token is intact even though entityDispatchState may diverge.
|
|
765
783
|
// If both are false, a newer wake owns the entity — leave status as-is.
|
|
766
784
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
767
|
-
await ctx.entityManager.registry.updateStatus(
|
|
785
|
+
await ctx.entityManager.registry.updateStatus(
|
|
786
|
+
entity.url,
|
|
787
|
+
entity.status === `stopping` ? `stopped` : `idle`
|
|
788
|
+
)
|
|
768
789
|
await ctx.entityBridgeManager.onEntityChanged(entity.url)
|
|
769
790
|
serverLog.info(
|
|
770
|
-
`[callback-forward] status updated
|
|
791
|
+
`[callback-forward] status updated after done for ${entity.url}`
|
|
771
792
|
)
|
|
772
793
|
} else if (!entity) {
|
|
773
794
|
serverLog.warn(
|
|
@@ -575,7 +575,7 @@ async function notificationFromClaim(
|
|
|
575
575
|
404
|
|
576
576
|
)
|
|
577
577
|
}
|
|
578
|
-
if (entity.status === `stopped`) {
|
|
578
|
+
if (entity.status === `stopped` || entity.status === `paused`) {
|
|
579
579
|
await ctx.streamClient.releaseSubscription(
|
|
580
580
|
input.subscriptionId,
|
|
581
581
|
input.claim.token,
|
package/src/runtime.ts
CHANGED
|
@@ -533,7 +533,11 @@ export class ElectricAgentsTenantRuntime {
|
|
|
533
533
|
return
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
-
await this.manager.registry.
|
|
536
|
+
const entity = await this.manager.registry.getEntity(entityUrl)
|
|
537
|
+
await this.manager.registry.updateStatus(
|
|
538
|
+
entityUrl,
|
|
539
|
+
entity?.status === `stopping` ? `stopped` : `idle`
|
|
540
|
+
)
|
|
537
541
|
await this.entityBridgeManager.onEntityChanged(entityUrl)
|
|
538
542
|
}
|
|
539
543
|
}
|
package/src/server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { Principal } from './principal.js'
|
|
|
34
34
|
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
35
35
|
import type { DurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
|
|
36
36
|
import type { OssServerContext } from './routing/oss-server-router.js'
|
|
37
|
+
import type { EventSourceCatalog } from './routing/context.js'
|
|
37
38
|
import type { StartedStandaloneAgentsRuntime } from './standalone-runtime.js'
|
|
38
39
|
import type { DurableStreamsBearerProvider } from './stream-client.js'
|
|
39
40
|
import type {
|
|
@@ -67,6 +68,8 @@ export interface ElectricAgentsServerOptions {
|
|
|
67
68
|
request: Request
|
|
68
69
|
) => Promise<Principal | null> | Principal | null
|
|
69
70
|
allowDevPrincipalFallback?: boolean
|
|
71
|
+
eventSources?: EventSourceCatalog
|
|
72
|
+
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
|
|
70
73
|
/**
|
|
71
74
|
* Disabled by default. When set to a positive interval, periodically
|
|
72
75
|
* recovers expired dispatch claims and stale outstanding wakes.
|
|
@@ -441,6 +444,15 @@ export class ElectricAgentsServer {
|
|
|
441
444
|
streamClient: this.streamClient,
|
|
442
445
|
runtime: this.standaloneRuntime.runtime,
|
|
443
446
|
entityBridgeManager: this.entityBridgeManager,
|
|
447
|
+
...(this.options.eventSources
|
|
448
|
+
? { eventSources: this.options.eventSources }
|
|
449
|
+
: {}),
|
|
450
|
+
...(this.options.ensureEventSourceWakeSource
|
|
451
|
+
? {
|
|
452
|
+
ensureEventSourceWakeSource:
|
|
453
|
+
this.options.ensureEventSourceWakeSource,
|
|
454
|
+
}
|
|
455
|
+
: {}),
|
|
444
456
|
isShuttingDown: () => this.shuttingDown,
|
|
445
457
|
mockAgent: this.mockAgentBootstrap
|
|
446
458
|
? { runtime: this.mockAgentBootstrap.runtime }
|