@electric-ax/agents-server 0.4.0 → 0.4.2
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 +381 -96
- package/dist/index.cjs +491 -198
- package/dist/index.d.cts +286 -244
- package/dist/index.d.ts +286 -244
- package/dist/index.js +491 -198
- package/drizzle/0006_principals.sql +5 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/authenticated-user-format.ts +4 -14
- package/src/db/index.ts +1 -1
- package/src/db/schema.ts +2 -0
- package/src/electric-agents-types.ts +10 -7
- package/src/entity-bridge-manager.ts +1 -1
- package/src/entity-manager.ts +223 -41
- package/src/entity-registry.ts +29 -0
- package/src/entrypoint-lib.ts +0 -9
- package/src/host.ts +4 -0
- package/src/index.ts +2 -1
- package/src/principal.ts +124 -0
- package/src/routing/context.ts +2 -2
- package/src/routing/dispatch-policy.ts +10 -10
- package/src/routing/entities-router.ts +180 -7
- package/src/routing/hooks.ts +1 -1
- package/src/routing/runners-router.ts +10 -18
- package/src/runtime.ts +4 -1
- package/src/scheduler.ts +2 -0
- package/src/server.ts +59 -7
- package/src/dev-asserted-auth.ts +0 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"pino-pretty": "^13.0.0",
|
|
55
55
|
"postgres": "^3.4.0",
|
|
56
56
|
"undici": "^7.24.7",
|
|
57
|
-
"@electric-ax/agents-runtime": "0.2.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.2.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.19.15",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"tsx": "^4.19.0",
|
|
66
66
|
"typescript": "^5.0.0",
|
|
67
67
|
"vitest": "^4.1.0",
|
|
68
|
-
"@electric-ax/agents
|
|
69
|
-
"@electric-ax/agents": "0.4
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.
|
|
68
|
+
"@electric-ax/agents": "0.4.1",
|
|
69
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.4",
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.2"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"scripts": {
|
|
81
81
|
"build": "tsdown",
|
|
82
82
|
"dev": "tsdown --watch",
|
|
83
|
-
"start": "cross-env-shell
|
|
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
84
|
"test": "vitest run",
|
|
85
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
86
|
"typecheck": "tsc --noEmit",
|
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Principal } from './principal.js'
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
return trimmed || undefined
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function formatAuthenticatedUser(
|
|
9
|
-
user: AuthenticatedRequestUser | null | undefined
|
|
3
|
+
export function formatRequestPrincipal(
|
|
4
|
+
principal: Principal | null | undefined
|
|
10
5
|
): string | undefined {
|
|
11
|
-
|
|
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
|
|
6
|
+
return principal?.key
|
|
17
7
|
}
|
package/src/db/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function createDb(postgresUrl: string): {
|
|
|
16
16
|
const poolMax = Number(process.env.ELECTRIC_AGENTS_PG_POOL_MAX ?? `100`)
|
|
17
17
|
const client = postgres(postgresUrl, {
|
|
18
18
|
max: poolMax,
|
|
19
|
-
fetch_types:
|
|
19
|
+
fetch_types: true,
|
|
20
20
|
})
|
|
21
21
|
const db = drizzle(client, { schema })
|
|
22
22
|
return { db, client }
|
package/src/db/schema.ts
CHANGED
|
@@ -50,6 +50,7 @@ export const entities = pgTable(
|
|
|
50
50
|
.default(sql`'{}'::text[]`),
|
|
51
51
|
spawnArgs: jsonb(`spawn_args`).default({}),
|
|
52
52
|
parent: text(`parent`),
|
|
53
|
+
createdBy: text(`created_by`),
|
|
53
54
|
typeRevision: integer(`type_revision`),
|
|
54
55
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
55
56
|
stateSchemas: jsonb(`state_schemas`),
|
|
@@ -61,6 +62,7 @@ export const entities = pgTable(
|
|
|
61
62
|
index(`idx_entities_type`).on(table.tenantId, table.type),
|
|
62
63
|
index(`idx_entities_status`).on(table.tenantId, table.status),
|
|
63
64
|
index(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
65
|
+
index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
64
66
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
65
67
|
check(
|
|
66
68
|
`chk_entities_status`,
|
|
@@ -3,18 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { WebhookNotification } from '@electric-ax/agents-runtime'
|
|
6
|
+
import type { Principal } from './principal.js'
|
|
6
7
|
|
|
7
8
|
type WakeNotification = WebhookNotification
|
|
8
9
|
|
|
9
|
-
export
|
|
10
|
-
userId: string
|
|
11
|
-
email?: string
|
|
12
|
-
name?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
10
|
+
export type RequestPrincipal = Principal
|
|
15
11
|
export type AuthenticateRequest = (
|
|
16
12
|
request: Request
|
|
17
|
-
) => Promise<
|
|
13
|
+
) => Promise<Principal | null> | Principal | null
|
|
18
14
|
|
|
19
15
|
export type EntityStatus = `spawning` | `running` | `idle` | `stopped`
|
|
20
16
|
|
|
@@ -211,6 +207,7 @@ export interface ElectricAgentsEntity {
|
|
|
211
207
|
type_revision?: number
|
|
212
208
|
inbox_schemas?: Record<string, Record<string, unknown>>
|
|
213
209
|
state_schemas?: Record<string, Record<string, unknown>>
|
|
210
|
+
created_by?: string
|
|
214
211
|
created_at: number
|
|
215
212
|
updated_at: number
|
|
216
213
|
}
|
|
@@ -225,6 +222,7 @@ export interface PublicElectricAgentsEntity {
|
|
|
225
222
|
tags: Record<string, string>
|
|
226
223
|
spawn_args?: Record<string, unknown>
|
|
227
224
|
parent?: string
|
|
225
|
+
created_by?: string
|
|
228
226
|
created_at: number
|
|
229
227
|
updated_at: number
|
|
230
228
|
}
|
|
@@ -248,6 +246,7 @@ export function toPublicEntity(
|
|
|
248
246
|
tags: entity.tags,
|
|
249
247
|
spawn_args: entity.spawn_args,
|
|
250
248
|
parent: entity.parent,
|
|
249
|
+
created_by: entity.created_by,
|
|
251
250
|
created_at: entity.created_at,
|
|
252
251
|
updated_at: entity.updated_at,
|
|
253
252
|
}
|
|
@@ -283,6 +282,7 @@ export interface TypedSpawnRequest {
|
|
|
283
282
|
parent?: string
|
|
284
283
|
dispatch_policy?: DispatchPolicy
|
|
285
284
|
initialMessage?: unknown
|
|
285
|
+
created_by?: string
|
|
286
286
|
wake?: {
|
|
287
287
|
subscriberUrl: string
|
|
288
288
|
condition:
|
|
@@ -303,6 +303,8 @@ export interface SendRequest {
|
|
|
303
303
|
payload?: unknown
|
|
304
304
|
key?: string
|
|
305
305
|
type?: string
|
|
306
|
+
mode?: `immediate` | `queued` | `paused` | `steer`
|
|
307
|
+
position?: string
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
export interface SetTagRequest {
|
|
@@ -312,6 +314,7 @@ export interface SetTagRequest {
|
|
|
312
314
|
export interface EntityListFilter {
|
|
313
315
|
type?: string
|
|
314
316
|
status?: EntityStatus
|
|
317
|
+
created_by?: string
|
|
315
318
|
}
|
|
316
319
|
|
|
317
320
|
export const ErrCodeDuplicateURL = `DUPLICATE_URL`
|
|
@@ -70,7 +70,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
70
70
|
] as const
|
|
71
71
|
|
|
72
72
|
function parseElectricOffset(offset: string): Offset | null {
|
|
73
|
-
if (offset === `-1`
|
|
73
|
+
if (offset === `-1`) {
|
|
74
74
|
return offset
|
|
75
75
|
}
|
|
76
76
|
return /^\d+_\d+$/.test(offset) ? (offset as Offset) : null
|
package/src/entity-manager.ts
CHANGED
|
@@ -27,6 +27,14 @@ import {
|
|
|
27
27
|
ErrCodeUnknownMessageType,
|
|
28
28
|
} from './electric-agents-types.js'
|
|
29
29
|
import { parseDispatchPolicy } from './dispatch-policy-schema.js'
|
|
30
|
+
import { applyTypeDefaultSubscriptionScope } from './routing/dispatch-policy.js'
|
|
31
|
+
import {
|
|
32
|
+
isBuiltInSystemPrincipalUrl,
|
|
33
|
+
principalFromCreatedBy,
|
|
34
|
+
principalUrl,
|
|
35
|
+
principalIdentityStateSchema,
|
|
36
|
+
principalUpdateIdentityMessageSchema,
|
|
37
|
+
} from './principal.js'
|
|
30
38
|
import { EntityAlreadyExistsError } from './entity-registry.js'
|
|
31
39
|
import { serverLog } from './utils/log.js'
|
|
32
40
|
import {
|
|
@@ -44,7 +52,6 @@ import type { SchemaValidator } from './electric-agents/schema-validator.js'
|
|
|
44
52
|
import type { StreamClient } from './stream-client.js'
|
|
45
53
|
import type {
|
|
46
54
|
DispatchPolicy,
|
|
47
|
-
DispatchTarget,
|
|
48
55
|
ElectricAgentsEntity,
|
|
49
56
|
ElectricAgentsEntityType,
|
|
50
57
|
RegisterEntityTypeRequest,
|
|
@@ -53,6 +60,7 @@ import type {
|
|
|
53
60
|
TypedSpawnRequest,
|
|
54
61
|
} from './electric-agents-types.js'
|
|
55
62
|
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
63
|
+
import type { Principal } from './principal.js'
|
|
56
64
|
|
|
57
65
|
type SpawnPersistResult = [
|
|
58
66
|
PromiseSettledResult<void>,
|
|
@@ -65,6 +73,10 @@ type WriteTokenValidator = (
|
|
|
65
73
|
token: string
|
|
66
74
|
) => boolean
|
|
67
75
|
|
|
76
|
+
function createInitialQueuePosition(date: Date): string {
|
|
77
|
+
return `${String(date.getTime()).padStart(16, `0`)}:a0`
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
type ForkSubtreeOptions = {
|
|
69
81
|
rootInstanceId?: string
|
|
70
82
|
waitTimeoutMs?: number
|
|
@@ -91,33 +103,6 @@ type ForkResult = {
|
|
|
91
103
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 120_000
|
|
92
104
|
const DEFAULT_FORK_WAIT_POLL_MS = 250
|
|
93
105
|
|
|
94
|
-
function applyTypeDefaultSubscriptionScope(
|
|
95
|
-
policy: DispatchPolicy,
|
|
96
|
-
typeDefault: DispatchPolicy | undefined
|
|
97
|
-
): DispatchPolicy {
|
|
98
|
-
const target = policy.targets[0]
|
|
99
|
-
const defaultTarget = typeDefault?.targets[0]
|
|
100
|
-
if (!target || !defaultTarget?.subscription_id) return policy
|
|
101
|
-
if (!sameDispatchDestination(target, defaultTarget)) return policy
|
|
102
|
-
if (target.subscription_id === defaultTarget.subscription_id) return policy
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
targets: [{ ...target, subscription_id: defaultTarget.subscription_id }],
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function sameDispatchDestination(
|
|
110
|
-
a: DispatchTarget,
|
|
111
|
-
b: DispatchTarget
|
|
112
|
-
): boolean {
|
|
113
|
-
if (a.type !== b.type) return false
|
|
114
|
-
if (a.type === `runner` && b.type === `runner`) {
|
|
115
|
-
return a.runnerId === b.runnerId
|
|
116
|
-
}
|
|
117
|
-
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url
|
|
118
|
-
return false
|
|
119
|
-
}
|
|
120
|
-
|
|
121
106
|
function sleep(ms: number): Promise<void> {
|
|
122
107
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
123
108
|
}
|
|
@@ -239,6 +224,13 @@ export class EntityManager {
|
|
|
239
224
|
400
|
|
240
225
|
)
|
|
241
226
|
}
|
|
227
|
+
if (req.name === `principal`) {
|
|
228
|
+
throw new ElectricAgentsError(
|
|
229
|
+
ErrCodeInvalidRequest,
|
|
230
|
+
`Entity type "principal" is built in and cannot be registered or updated`,
|
|
231
|
+
400
|
|
232
|
+
)
|
|
233
|
+
}
|
|
242
234
|
if (req.name.startsWith(`_`)) {
|
|
243
235
|
throw new ElectricAgentsError(
|
|
244
236
|
ErrCodeInvalidRequest,
|
|
@@ -290,6 +282,13 @@ export class EntityManager {
|
|
|
290
282
|
}
|
|
291
283
|
|
|
292
284
|
async deleteEntityType(name: string): Promise<void> {
|
|
285
|
+
if (name === `principal`) {
|
|
286
|
+
throw new ElectricAgentsError(
|
|
287
|
+
ErrCodeInvalidRequest,
|
|
288
|
+
`Entity type "principal" is built in and cannot be deleted`,
|
|
289
|
+
400
|
|
290
|
+
)
|
|
291
|
+
}
|
|
293
292
|
const existing = await this.registry.getEntityType(name)
|
|
294
293
|
if (!existing) {
|
|
295
294
|
throw new ElectricAgentsError(
|
|
@@ -302,6 +301,59 @@ export class EntityManager {
|
|
|
302
301
|
await this.registry.deleteEntityType(name)
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
async ensurePrincipalEntityType(): Promise<ElectricAgentsEntityType> {
|
|
305
|
+
const now = new Date().toISOString()
|
|
306
|
+
return await this.registry.ensureEntityType({
|
|
307
|
+
name: `principal`,
|
|
308
|
+
description: `built-in principal entity`,
|
|
309
|
+
inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
|
|
310
|
+
state_schemas: { identity: principalIdentityStateSchema },
|
|
311
|
+
revision: 1,
|
|
312
|
+
created_at: now,
|
|
313
|
+
updated_at: now,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async ensurePrincipal(principal: Principal): Promise<ElectricAgentsEntity> {
|
|
318
|
+
const existing = await this.registry.getEntity(principal.url)
|
|
319
|
+
if (existing) return existing
|
|
320
|
+
await this.ensurePrincipalEntityType()
|
|
321
|
+
try {
|
|
322
|
+
const entity = await this.spawn(`principal`, {
|
|
323
|
+
instance_id: principal.key,
|
|
324
|
+
args: { kind: principal.kind, id: principal.id, key: principal.key },
|
|
325
|
+
tags: { principal_kind: principal.kind, principal_id: principal.id },
|
|
326
|
+
created_by: principal.url,
|
|
327
|
+
})
|
|
328
|
+
const now = new Date().toISOString()
|
|
329
|
+
await this.streamClient.append(
|
|
330
|
+
entity.streams.main,
|
|
331
|
+
this.encodeChangeEvent({
|
|
332
|
+
type: `identity`,
|
|
333
|
+
key: `self`,
|
|
334
|
+
value: {
|
|
335
|
+
kind: principal.kind,
|
|
336
|
+
id: principal.id,
|
|
337
|
+
key: principal.key,
|
|
338
|
+
url: principal.url,
|
|
339
|
+
created_at: now,
|
|
340
|
+
updated_at: now,
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
)
|
|
344
|
+
return entity
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (
|
|
347
|
+
error instanceof ElectricAgentsError &&
|
|
348
|
+
error.code === ErrCodeDuplicateURL
|
|
349
|
+
) {
|
|
350
|
+
const raced = await this.registry.getEntity(principal.url)
|
|
351
|
+
if (raced) return raced
|
|
352
|
+
}
|
|
353
|
+
throw error
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
305
357
|
// ==========================================================================
|
|
306
358
|
// Spawn
|
|
307
359
|
// ==========================================================================
|
|
@@ -328,6 +380,17 @@ export class EntityManager {
|
|
|
328
380
|
typeName: string,
|
|
329
381
|
req: TypedSpawnRequest
|
|
330
382
|
): Promise<ElectricAgentsEntity & { txid: number }> {
|
|
383
|
+
if (
|
|
384
|
+
typeName === `principal` &&
|
|
385
|
+
req.created_by !== principalUrl(req.instance_id)
|
|
386
|
+
) {
|
|
387
|
+
throw new ElectricAgentsError(
|
|
388
|
+
ErrCodeInvalidRequest,
|
|
389
|
+
`Principal entities are built in and can only be materialized by the system`,
|
|
390
|
+
400
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
331
394
|
if (typeName.startsWith(`_`)) {
|
|
332
395
|
throw new ElectricAgentsError(
|
|
333
396
|
ErrCodeInvalidRequest,
|
|
@@ -375,7 +438,10 @@ export class EntityManager {
|
|
|
375
438
|
|
|
376
439
|
const writeToken = randomUUID()
|
|
377
440
|
|
|
378
|
-
const entityURL =
|
|
441
|
+
const entityURL =
|
|
442
|
+
typeName === `principal`
|
|
443
|
+
? principalUrl(instanceId)
|
|
444
|
+
: `/${typeName}/${instanceId}`
|
|
379
445
|
const mainPath = `${entityURL}/main`
|
|
380
446
|
const errorPath = `${entityURL}/error`
|
|
381
447
|
|
|
@@ -433,6 +499,7 @@ export class EntityManager {
|
|
|
433
499
|
inbox_schemas: entityType.inbox_schemas,
|
|
434
500
|
state_schemas: entityType.state_schemas,
|
|
435
501
|
created_at: now,
|
|
502
|
+
created_by: req.created_by ?? parentEntity?.created_by,
|
|
436
503
|
updated_at: now,
|
|
437
504
|
}
|
|
438
505
|
if (req.parent) {
|
|
@@ -473,7 +540,7 @@ export class EntityManager {
|
|
|
473
540
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
474
541
|
key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
475
542
|
value: {
|
|
476
|
-
from: req.parent ?? `spawn`,
|
|
543
|
+
from: req.created_by ?? req.parent ?? `spawn`,
|
|
477
544
|
payload: req.initialMessage,
|
|
478
545
|
timestamp: msgNow,
|
|
479
546
|
},
|
|
@@ -1371,10 +1438,10 @@ export class EntityManager {
|
|
|
1371
1438
|
changed = true
|
|
1372
1439
|
}
|
|
1373
1440
|
}
|
|
1374
|
-
if (typeof next.
|
|
1375
|
-
const
|
|
1376
|
-
if (
|
|
1377
|
-
next.
|
|
1441
|
+
if (typeof next.senderUrl === `string`) {
|
|
1442
|
+
const forkSender = entityUrlMap.get(next.senderUrl)
|
|
1443
|
+
if (forkSender) {
|
|
1444
|
+
next.senderUrl = forkSender
|
|
1378
1445
|
changed = true
|
|
1379
1446
|
}
|
|
1380
1447
|
}
|
|
@@ -1490,6 +1557,10 @@ export class EntityManager {
|
|
|
1490
1557
|
const fireAtRaw = manifest.fireAt
|
|
1491
1558
|
const producerId = manifest.producerId
|
|
1492
1559
|
const targetUrl = manifest.targetUrl
|
|
1560
|
+
const senderUrl =
|
|
1561
|
+
typeof manifest.senderUrl === `string`
|
|
1562
|
+
? manifest.senderUrl
|
|
1563
|
+
: ownerEntityUrl
|
|
1493
1564
|
if (
|
|
1494
1565
|
typeof fireAtRaw !== `string` ||
|
|
1495
1566
|
typeof producerId !== `string` ||
|
|
@@ -1514,8 +1585,7 @@ export class EntityManager {
|
|
|
1514
1585
|
manifestKey,
|
|
1515
1586
|
{
|
|
1516
1587
|
entityUrl: targetUrl,
|
|
1517
|
-
from:
|
|
1518
|
-
typeof manifest.from === `string` ? manifest.from : ownerEntityUrl,
|
|
1588
|
+
from: senderUrl,
|
|
1519
1589
|
payload: manifest.payload,
|
|
1520
1590
|
key: `scheduled-${producerId}`,
|
|
1521
1591
|
type:
|
|
@@ -1532,6 +1602,7 @@ export class EntityManager {
|
|
|
1532
1602
|
kind: `schedule`,
|
|
1533
1603
|
scheduleType: `future_send`,
|
|
1534
1604
|
targetUrl,
|
|
1605
|
+
senderUrl,
|
|
1535
1606
|
fireAt: fireAt.toISOString(),
|
|
1536
1607
|
producerId,
|
|
1537
1608
|
status: `pending`,
|
|
@@ -1584,10 +1655,23 @@ export class EntityManager {
|
|
|
1584
1655
|
from: req.from,
|
|
1585
1656
|
payload: req.payload,
|
|
1586
1657
|
timestamp: now,
|
|
1658
|
+
mode: req.mode ?? `immediate`,
|
|
1659
|
+
status:
|
|
1660
|
+
req.mode === `queued` || req.mode === `paused`
|
|
1661
|
+
? `pending`
|
|
1662
|
+
: `processed`,
|
|
1587
1663
|
}
|
|
1588
1664
|
if (req.type) {
|
|
1589
1665
|
value.message_type = req.type
|
|
1590
1666
|
}
|
|
1667
|
+
if (req.position) {
|
|
1668
|
+
value.position = req.position
|
|
1669
|
+
} else if (value.mode === `queued` || value.mode === `paused`) {
|
|
1670
|
+
value.position = createInitialQueuePosition(new Date(now))
|
|
1671
|
+
}
|
|
1672
|
+
if (value.status === `processed`) {
|
|
1673
|
+
value.processed_at = now
|
|
1674
|
+
}
|
|
1591
1675
|
|
|
1592
1676
|
const envelope = entityStateSchema.inbox.insert({
|
|
1593
1677
|
key,
|
|
@@ -1604,6 +1688,17 @@ export class EntityManager {
|
|
|
1604
1688
|
}
|
|
1605
1689
|
|
|
1606
1690
|
await this.streamClient.append(entity.streams.main, encoded)
|
|
1691
|
+
if (entity.type === `principal` && req.type === `update_identity`) {
|
|
1692
|
+
const identity = (req.payload as { identity?: unknown })?.identity
|
|
1693
|
+
await this.streamClient.append(
|
|
1694
|
+
entity.streams.main,
|
|
1695
|
+
this.encodeChangeEvent({
|
|
1696
|
+
type: `identity`,
|
|
1697
|
+
key: `self`,
|
|
1698
|
+
value: identity,
|
|
1699
|
+
})
|
|
1700
|
+
)
|
|
1701
|
+
}
|
|
1607
1702
|
} catch (err) {
|
|
1608
1703
|
if (this.isClosedStreamError(err)) {
|
|
1609
1704
|
throw new ElectricAgentsError(
|
|
@@ -1616,6 +1711,69 @@ export class EntityManager {
|
|
|
1616
1711
|
}
|
|
1617
1712
|
}
|
|
1618
1713
|
|
|
1714
|
+
async updateInboxMessage(
|
|
1715
|
+
entityUrl: string,
|
|
1716
|
+
key: string,
|
|
1717
|
+
req: {
|
|
1718
|
+
payload?: unknown
|
|
1719
|
+
position?: string
|
|
1720
|
+
mode?: `immediate` | `queued` | `paused` | `steer`
|
|
1721
|
+
status?: `pending` | `processed` | `cancelled`
|
|
1722
|
+
}
|
|
1723
|
+
): Promise<void> {
|
|
1724
|
+
const entity = await this.registry.getEntity(entityUrl)
|
|
1725
|
+
if (!entity) {
|
|
1726
|
+
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
1727
|
+
}
|
|
1728
|
+
if (entity.status === `stopped`) {
|
|
1729
|
+
throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const now = new Date().toISOString()
|
|
1733
|
+
const value: Record<string, unknown> = {}
|
|
1734
|
+
if (`payload` in req) value.payload = req.payload
|
|
1735
|
+
if (req.position !== undefined) value.position = req.position
|
|
1736
|
+
if (req.mode !== undefined) value.mode = req.mode
|
|
1737
|
+
if (req.status !== undefined) {
|
|
1738
|
+
value.status = req.status
|
|
1739
|
+
if (req.status === `processed`) value.processed_at = now
|
|
1740
|
+
if (req.status === `cancelled`) value.cancelled_at = now
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
if (Object.keys(value).length === 0) {
|
|
1744
|
+
throw new ElectricAgentsError(
|
|
1745
|
+
ErrCodeInvalidRequest,
|
|
1746
|
+
`No inbox fields to update`,
|
|
1747
|
+
400
|
|
1748
|
+
)
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const envelope = entityStateSchema.inbox.update({
|
|
1752
|
+
key,
|
|
1753
|
+
value,
|
|
1754
|
+
} as any)
|
|
1755
|
+
await this.streamClient.append(
|
|
1756
|
+
entity.streams.main,
|
|
1757
|
+
this.encodeChangeEvent(envelope as Record<string, unknown>)
|
|
1758
|
+
)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
async deleteInboxMessage(entityUrl: string, key: string): Promise<void> {
|
|
1762
|
+
const entity = await this.registry.getEntity(entityUrl)
|
|
1763
|
+
if (!entity) {
|
|
1764
|
+
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
1765
|
+
}
|
|
1766
|
+
if (entity.status === `stopped`) {
|
|
1767
|
+
throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const envelope = entityStateSchema.inbox.delete({ key } as any)
|
|
1771
|
+
await this.streamClient.append(
|
|
1772
|
+
entity.streams.main,
|
|
1773
|
+
this.encodeChangeEvent(envelope as Record<string, unknown>)
|
|
1774
|
+
)
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1619
1777
|
// ==========================================================================
|
|
1620
1778
|
// Tag Updates
|
|
1621
1779
|
// ==========================================================================
|
|
@@ -1830,7 +1988,7 @@ export class EntityManager {
|
|
|
1830
1988
|
payload: unknown
|
|
1831
1989
|
targetUrl?: string
|
|
1832
1990
|
fireAt: string
|
|
1833
|
-
|
|
1991
|
+
senderUrl?: string
|
|
1834
1992
|
messageType?: string
|
|
1835
1993
|
}
|
|
1836
1994
|
): Promise<{ txid: string }> {
|
|
@@ -1839,7 +1997,7 @@ export class EntityManager {
|
|
|
1839
1997
|
}
|
|
1840
1998
|
|
|
1841
1999
|
const targetUrl = req.targetUrl ?? ownerEntityUrl
|
|
1842
|
-
const from = req.
|
|
2000
|
+
const from = req.senderUrl ?? ownerEntityUrl
|
|
1843
2001
|
const fireAt = new Date(req.fireAt)
|
|
1844
2002
|
if (Number.isNaN(fireAt.getTime())) {
|
|
1845
2003
|
throw new ElectricAgentsError(
|
|
@@ -1883,9 +2041,9 @@ export class EntityManager {
|
|
|
1883
2041
|
scheduleType: `future_send`,
|
|
1884
2042
|
fireAt: fireAt.toISOString(),
|
|
1885
2043
|
targetUrl,
|
|
2044
|
+
senderUrl: from,
|
|
1886
2045
|
payload: req.payload,
|
|
1887
2046
|
producerId,
|
|
1888
|
-
...(req.from ? { from: req.from } : {}),
|
|
1889
2047
|
...(req.messageType ? { messageType: req.messageType } : {}),
|
|
1890
2048
|
status: `pending`,
|
|
1891
2049
|
},
|
|
@@ -1906,9 +2064,9 @@ export class EntityManager {
|
|
|
1906
2064
|
scheduleType: `future_send`,
|
|
1907
2065
|
fireAt: fireAt.toISOString(),
|
|
1908
2066
|
targetUrl,
|
|
2067
|
+
senderUrl: from,
|
|
1909
2068
|
payload: req.payload,
|
|
1910
2069
|
producerId,
|
|
1911
|
-
...(req.from ? { from: req.from } : {}),
|
|
1912
2070
|
...(req.messageType ? { messageType: req.messageType } : {}),
|
|
1913
2071
|
status: `pending`,
|
|
1914
2072
|
},
|
|
@@ -1987,6 +2145,8 @@ export class EntityManager {
|
|
|
1987
2145
|
payload: req.payload,
|
|
1988
2146
|
key: req.key,
|
|
1989
2147
|
type: req.type,
|
|
2148
|
+
mode: req.mode,
|
|
2149
|
+
position: req.position,
|
|
1990
2150
|
},
|
|
1991
2151
|
fireAt
|
|
1992
2152
|
)
|
|
@@ -2308,6 +2468,14 @@ export class EntityManager {
|
|
|
2308
2468
|
state_schemas?: Record<string, Record<string, unknown>>
|
|
2309
2469
|
}
|
|
2310
2470
|
): Promise<ElectricAgentsEntityType> {
|
|
2471
|
+
if (typeName === `principal`) {
|
|
2472
|
+
throw new ElectricAgentsError(
|
|
2473
|
+
ErrCodeInvalidRequest,
|
|
2474
|
+
`Entity type "principal" is built in and cannot be amended`,
|
|
2475
|
+
400
|
|
2476
|
+
)
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2311
2479
|
// Validate each provided schema via validateSchemaSubset.
|
|
2312
2480
|
this.validateSchemaMap(schemas.inbox_schemas)
|
|
2313
2481
|
this.validateSchemaMap(schemas.state_schemas)
|
|
@@ -2400,8 +2568,10 @@ export class EntityManager {
|
|
|
2400
2568
|
streams: entity.streams,
|
|
2401
2569
|
tags: entity.tags,
|
|
2402
2570
|
spawnArgs: entity.spawn_args,
|
|
2571
|
+
createdBy: entity.created_by,
|
|
2403
2572
|
},
|
|
2404
|
-
|
|
2573
|
+
principal: principalFromCreatedBy(entity.created_by),
|
|
2574
|
+
triggerEvent: `inbox`,
|
|
2405
2575
|
}
|
|
2406
2576
|
}
|
|
2407
2577
|
|
|
@@ -2491,6 +2661,18 @@ export class EntityManager {
|
|
|
2491
2661
|
400
|
|
2492
2662
|
)
|
|
2493
2663
|
}
|
|
2664
|
+
if (
|
|
2665
|
+
entity.type === `principal` &&
|
|
2666
|
+
req.type === `update_identity` &&
|
|
2667
|
+
!isBuiltInSystemPrincipalUrl(req.from)
|
|
2668
|
+
) {
|
|
2669
|
+
throw new ElectricAgentsError(
|
|
2670
|
+
ErrCodeUnauthorized,
|
|
2671
|
+
`Only built-in system principals can update principal identity`,
|
|
2672
|
+
403
|
|
2673
|
+
)
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2494
2676
|
if (req.payload === undefined) {
|
|
2495
2677
|
throw new ElectricAgentsError(
|
|
2496
2678
|
ErrCodeInvalidRequest,
|