@electric-ax/agents-server 0.4.16 → 0.4.18
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 +169 -12
- package/dist/index.cjs +168 -11
- package/dist/index.d.cts +304 -236
- package/dist/index.d.ts +304 -236
- package/dist/index.js +169 -12
- package/drizzle/0014_entity_type_slash_commands.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/db/schema.ts +1 -0
- package/src/electric-agents-types.ts +4 -0
- package/src/entity-manager.ts +210 -12
- package/src/entity-registry.ts +7 -0
- package/src/routing/entities-router.ts +122 -1
- package/src/routing/entity-types-router.ts +23 -0
- package/src/utils/server-utils.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import postgres from "postgres";
|
|
|
8
8
|
import { and, desc, eq, inArray, lt, ne, sql } from "drizzle-orm";
|
|
9
9
|
import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
10
10
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from "node:crypto";
|
|
11
|
-
import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
11
|
+
import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
12
12
|
import { DurableStream, DurableStreamError, FetchError, IdempotentProducer } from "@durable-streams/client";
|
|
13
13
|
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
14
14
|
import pino from "pino";
|
|
@@ -49,6 +49,7 @@ const entityTypes = pgTable(`entity_types`, {
|
|
|
49
49
|
creationSchema: jsonb(`creation_schema`),
|
|
50
50
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
51
51
|
stateSchemas: jsonb(`state_schemas`),
|
|
52
|
+
slashCommands: jsonb(`slash_commands`),
|
|
52
53
|
serveEndpoint: text(`serve_endpoint`),
|
|
53
54
|
defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
|
|
54
55
|
revision: integer(`revision`).notNull().default(1),
|
|
@@ -820,6 +821,7 @@ var PostgresRegistry = class {
|
|
|
820
821
|
creationSchema: et.creation_schema ?? null,
|
|
821
822
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
822
823
|
stateSchemas: et.state_schemas ?? null,
|
|
824
|
+
slashCommands: et.slash_commands ?? null,
|
|
823
825
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
824
826
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
825
827
|
revision: et.revision,
|
|
@@ -832,6 +834,7 @@ var PostgresRegistry = class {
|
|
|
832
834
|
creationSchema: et.creation_schema ?? null,
|
|
833
835
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
834
836
|
stateSchemas: et.state_schemas ?? null,
|
|
837
|
+
slashCommands: et.slash_commands ?? null,
|
|
835
838
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
836
839
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
837
840
|
revision: et.revision,
|
|
@@ -849,6 +852,7 @@ var PostgresRegistry = class {
|
|
|
849
852
|
creationSchema: et.creation_schema ?? null,
|
|
850
853
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
851
854
|
stateSchemas: et.state_schemas ?? null,
|
|
855
|
+
slashCommands: et.slash_commands ?? null,
|
|
852
856
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
853
857
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
854
858
|
revision: et.revision,
|
|
@@ -875,6 +879,7 @@ var PostgresRegistry = class {
|
|
|
875
879
|
creationSchema: et.creation_schema ?? null,
|
|
876
880
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
877
881
|
stateSchemas: et.state_schemas ?? null,
|
|
882
|
+
slashCommands: et.slash_commands ?? null,
|
|
878
883
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
879
884
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
880
885
|
revision: et.revision,
|
|
@@ -1465,6 +1470,7 @@ var PostgresRegistry = class {
|
|
|
1465
1470
|
creation_schema: row.creationSchema,
|
|
1466
1471
|
inbox_schemas: row.inboxSchemas,
|
|
1467
1472
|
state_schemas: row.stateSchemas,
|
|
1473
|
+
slash_commands: row.slashCommands ?? void 0,
|
|
1468
1474
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
1469
1475
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
1470
1476
|
revision: row.revision,
|
|
@@ -3358,6 +3364,7 @@ var EntityManager = class {
|
|
|
3358
3364
|
this.validateSchema(req.creation_schema);
|
|
3359
3365
|
this.validateSchemaMap(req.inbox_schemas);
|
|
3360
3366
|
this.validateSchemaMap(req.state_schemas);
|
|
3367
|
+
this.validateSlashCommands(req.slash_commands);
|
|
3361
3368
|
const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
|
|
3362
3369
|
const existing = await this.registry.getEntityType(req.name);
|
|
3363
3370
|
const now = new Date().toISOString();
|
|
@@ -3367,6 +3374,7 @@ var EntityManager = class {
|
|
|
3367
3374
|
creation_schema: req.creation_schema,
|
|
3368
3375
|
inbox_schemas: req.inbox_schemas,
|
|
3369
3376
|
state_schemas: req.state_schemas,
|
|
3377
|
+
slash_commands: req.slash_commands,
|
|
3370
3378
|
serve_endpoint: req.serve_endpoint,
|
|
3371
3379
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
3372
3380
|
revision: existing ? existing.revision + 1 : 1,
|
|
@@ -3528,6 +3536,18 @@ var EntityManager = class {
|
|
|
3528
3536
|
}
|
|
3529
3537
|
});
|
|
3530
3538
|
const initialEvents = [createdEvent];
|
|
3539
|
+
const slashCommandTimestamp = new Date().toISOString();
|
|
3540
|
+
for (const command of entityType.slash_commands ?? []) {
|
|
3541
|
+
const slashCommandEvent = entityStateSchema.slashCommands.insert({
|
|
3542
|
+
key: command.name,
|
|
3543
|
+
value: {
|
|
3544
|
+
...command,
|
|
3545
|
+
source: `static`,
|
|
3546
|
+
updated_at: slashCommandTimestamp
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3549
|
+
initialEvents.push(slashCommandEvent);
|
|
3550
|
+
}
|
|
3531
3551
|
if (req.initialMessage !== void 0) {
|
|
3532
3552
|
const msgNow = new Date().toISOString();
|
|
3533
3553
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
@@ -3535,6 +3555,7 @@ var EntityManager = class {
|
|
|
3535
3555
|
value: {
|
|
3536
3556
|
from: req.created_by ?? req.parent ?? `spawn`,
|
|
3537
3557
|
payload: req.initialMessage,
|
|
3558
|
+
message_type: req.initialMessageType,
|
|
3538
3559
|
timestamp: msgNow
|
|
3539
3560
|
}
|
|
3540
3561
|
});
|
|
@@ -3614,9 +3635,10 @@ var EntityManager = class {
|
|
|
3614
3635
|
const workLocks = new Set();
|
|
3615
3636
|
const writeEntityLocks = new Set();
|
|
3616
3637
|
const writeStreamLocks = new Set();
|
|
3638
|
+
const usePointerPath = opts.forkPointer !== void 0 || opts.anchor !== void 0;
|
|
3617
3639
|
try {
|
|
3618
3640
|
let sourceTree;
|
|
3619
|
-
if (
|
|
3641
|
+
if (usePointerPath) {
|
|
3620
3642
|
const rootEntity = await this.registry.getEntity(rootUrl);
|
|
3621
3643
|
if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3622
3644
|
if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
|
|
@@ -3625,10 +3647,13 @@ var EntityManager = class {
|
|
|
3625
3647
|
const sourceRoot = sourceTree[0];
|
|
3626
3648
|
if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3627
3649
|
let preFilteredRoot;
|
|
3628
|
-
|
|
3650
|
+
let effectiveForkPointer = opts.forkPointer;
|
|
3651
|
+
if (usePointerPath) {
|
|
3629
3652
|
const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
|
|
3630
3653
|
const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
|
|
3631
|
-
|
|
3654
|
+
if (!effectiveForkPointer && opts.anchor === `latest_completed_run`) effectiveForkPointer = this.resolveLatestCompletedRunPointer(flat, sourceRoot.streams.main);
|
|
3655
|
+
if (!effectiveForkPointer) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Internal: pointer-path fork with no resolved pointer`, 500);
|
|
3656
|
+
const target = this.resolveForkPointerTarget(flat, effectiveForkPointer, sourceRoot.streams.main);
|
|
3632
3657
|
const filteredEvents = flat.slice(0, target);
|
|
3633
3658
|
const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
|
|
3634
3659
|
const sharedStateIds = new Set();
|
|
@@ -3641,7 +3666,7 @@ var EntityManager = class {
|
|
|
3641
3666
|
};
|
|
3642
3667
|
}
|
|
3643
3668
|
const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
|
|
3644
|
-
if (
|
|
3669
|
+
if (usePointerPath) {
|
|
3645
3670
|
const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
|
|
3646
3671
|
if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
|
|
3647
3672
|
}
|
|
@@ -3666,6 +3691,14 @@ var EntityManager = class {
|
|
|
3666
3691
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3667
3692
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3668
3693
|
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
3694
|
+
const rootPlanForOverrides = entityPlans.find((plan) => plan.source.url === rootUrl);
|
|
3695
|
+
if (rootPlanForOverrides) {
|
|
3696
|
+
if (opts.parent !== void 0) rootPlanForOverrides.fork.parent = opts.parent;
|
|
3697
|
+
if (opts.tags !== void 0) rootPlanForOverrides.fork.tags = {
|
|
3698
|
+
...rootPlanForOverrides.fork.tags ?? {},
|
|
3699
|
+
...opts.tags
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3669
3702
|
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3670
3703
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
|
|
3671
3704
|
const createdStreams = [];
|
|
@@ -3674,7 +3707,7 @@ var EntityManager = class {
|
|
|
3674
3707
|
try {
|
|
3675
3708
|
for (const plan of entityPlans) {
|
|
3676
3709
|
const isRoot = plan.source.url === rootUrl;
|
|
3677
|
-
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot &&
|
|
3710
|
+
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && effectiveForkPointer ? { forkPointer: effectiveForkPointer } : void 0);
|
|
3678
3711
|
createdStreams.push(plan.fork.streams.main);
|
|
3679
3712
|
}
|
|
3680
3713
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
@@ -3701,6 +3734,20 @@ var EntityManager = class {
|
|
|
3701
3734
|
await this.registry.createEntity(plan.fork);
|
|
3702
3735
|
createdEntities.push(plan.fork.url);
|
|
3703
3736
|
}
|
|
3737
|
+
if (opts.wake !== void 0) {
|
|
3738
|
+
const rootPlan = entityPlans.find((plan) => plan.source.url === rootUrl);
|
|
3739
|
+
if (rootPlan) await this.wakeRegistry.register({
|
|
3740
|
+
tenantId: this.tenantId,
|
|
3741
|
+
subscriberUrl: opts.wake.subscriberUrl,
|
|
3742
|
+
sourceUrl: rootPlan.fork.url,
|
|
3743
|
+
condition: opts.wake.condition,
|
|
3744
|
+
debounceMs: opts.wake.debounceMs,
|
|
3745
|
+
timeoutMs: opts.wake.timeoutMs,
|
|
3746
|
+
oneShot: false,
|
|
3747
|
+
includeResponse: opts.wake.includeResponse,
|
|
3748
|
+
manifestKey: opts.wake.manifestKey
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3704
3751
|
for (const plan of entityPlans) {
|
|
3705
3752
|
const manifests = activeManifestsByEntity.get(plan.fork.url) ?? new Map();
|
|
3706
3753
|
await this.materializeForkManifestSideEffects(plan.fork.url, manifests);
|
|
@@ -3861,6 +3908,45 @@ var EntityManager = class {
|
|
|
3861
3908
|
return positionAtAnchor + pointer.subOffset;
|
|
3862
3909
|
}
|
|
3863
3910
|
/**
|
|
3911
|
+
* Find an `EventPointer` that addresses the most recent `runs` row on
|
|
3912
|
+
* the source's `main` stream with `status === 'completed'`. Mirrors the
|
|
3913
|
+
* eligibility rule the per-row "Fork from here" UI applies — only
|
|
3914
|
+
* completed runs are valid fork anchors; `started`/`failed` rows are
|
|
3915
|
+
* skipped.
|
|
3916
|
+
*
|
|
3917
|
+
* The pointer is computed in the same coordinate system the runtime
|
|
3918
|
+
* mints pointers in (see entity-stream-db's onBeforeBatch): for a row
|
|
3919
|
+
* R in log entry E, the pointer is `{ offset: P, subOffset: K }` where
|
|
3920
|
+
* `P` is the END offset of the log entry preceding E (or `null` for
|
|
3921
|
+
* stream start) and `K` is R's 1-indexed position within E.
|
|
3922
|
+
*/
|
|
3923
|
+
resolveLatestCompletedRunPointer(events, streamPath) {
|
|
3924
|
+
let priorEntryOffset = null;
|
|
3925
|
+
let currentEntryOffset = null;
|
|
3926
|
+
let positionInEntry = 0;
|
|
3927
|
+
let latest = null;
|
|
3928
|
+
for (const event of events) {
|
|
3929
|
+
const headers = isRecord(event.headers) ? event.headers : void 0;
|
|
3930
|
+
const eventOffset = typeof headers?.offset === `string` ? headers.offset : null;
|
|
3931
|
+
if (eventOffset !== currentEntryOffset) {
|
|
3932
|
+
priorEntryOffset = currentEntryOffset;
|
|
3933
|
+
currentEntryOffset = eventOffset;
|
|
3934
|
+
positionInEntry = 0;
|
|
3935
|
+
}
|
|
3936
|
+
positionInEntry++;
|
|
3937
|
+
if (event.type !== `run`) continue;
|
|
3938
|
+
if (headers?.operation === `delete`) continue;
|
|
3939
|
+
if (!isRecord(event.value)) continue;
|
|
3940
|
+
if (event.value.status !== `completed`) continue;
|
|
3941
|
+
latest = {
|
|
3942
|
+
offset: priorEntryOffset,
|
|
3943
|
+
subOffset: positionInEntry
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
if (!latest) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Source ${streamPath} has no completed run to fork from`, 400);
|
|
3947
|
+
return latest;
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3864
3950
|
* Compute the subset of `sourceTree` that survives the manifest filter
|
|
3865
3951
|
* applied at the root. After filtering the root's manifest at the fork
|
|
3866
3952
|
* pointer, only children whose manifest entries landed at or before the
|
|
@@ -4980,7 +5066,9 @@ var EntityManager = class {
|
|
|
4980
5066
|
creation_schema: existing.creation_schema,
|
|
4981
5067
|
inbox_schemas: mergedInbox,
|
|
4982
5068
|
state_schemas: mergedState,
|
|
5069
|
+
slash_commands: existing.slash_commands,
|
|
4983
5070
|
serve_endpoint: existing.serve_endpoint,
|
|
5071
|
+
default_dispatch_policy: existing.default_dispatch_policy,
|
|
4984
5072
|
revision: nextRevision,
|
|
4985
5073
|
created_at: existing.created_at,
|
|
4986
5074
|
updated_at: now
|
|
@@ -5034,11 +5122,19 @@ var EntityManager = class {
|
|
|
5034
5122
|
throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
|
|
5035
5123
|
}
|
|
5036
5124
|
}
|
|
5125
|
+
validateSlashCommands(input) {
|
|
5126
|
+
const validationError = validateSlashCommandDefinitions(input);
|
|
5127
|
+
if (!validationError) return;
|
|
5128
|
+
throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
|
|
5129
|
+
}
|
|
5037
5130
|
async validateSendRequest(entityUrl, req) {
|
|
5038
5131
|
const entity = await this.registry.getEntity(entityUrl);
|
|
5039
5132
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
5040
5133
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
5041
|
-
if (req.type
|
|
5134
|
+
if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
|
|
5135
|
+
const valErr = validateComposerInputPayload(req.payload);
|
|
5136
|
+
if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
|
|
5137
|
+
} else if (req.type && entity.type) {
|
|
5042
5138
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
5043
5139
|
if (inboxSchemas) {
|
|
5044
5140
|
const schema = inboxSchemas[req.type];
|
|
@@ -7111,7 +7207,7 @@ function buildElectricProxyTarget(options) {
|
|
|
7111
7207
|
permissionBypass: options.permissionBypass
|
|
7112
7208
|
}));
|
|
7113
7209
|
} else if (table === `entity_types`) {
|
|
7114
|
-
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
7210
|
+
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","slash_commands","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
7115
7211
|
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
7116
7212
|
tenantId: options.tenantId,
|
|
7117
7213
|
principalUrl: options.principalUrl ?? ``,
|
|
@@ -7919,6 +8015,7 @@ const spawnBodySchema = Type.Object({
|
|
|
7919
8015
|
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
7920
8016
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
7921
8017
|
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
8018
|
+
initialMessageType: Type.Optional(Type.String()),
|
|
7922
8019
|
wake: Type.Optional(Type.Object({
|
|
7923
8020
|
subscriberUrl: Type.String(),
|
|
7924
8021
|
condition: wakeConditionSchema,
|
|
@@ -7956,6 +8053,14 @@ function agentUrlPath(value) {
|
|
|
7956
8053
|
return value;
|
|
7957
8054
|
}
|
|
7958
8055
|
}
|
|
8056
|
+
async function hasValidAgentWriteToken(request, ctx, fromAgent) {
|
|
8057
|
+
const agentUrl = agentUrlPath(fromAgent);
|
|
8058
|
+
const token = writeTokenFromRequest(request);
|
|
8059
|
+
if (!token) return false;
|
|
8060
|
+
const agentEntity = await ctx.entityManager.registry.getEntity(agentUrl);
|
|
8061
|
+
if (!agentEntity) return false;
|
|
8062
|
+
return ctx.entityManager.isValidWriteToken(agentEntity, token);
|
|
8063
|
+
}
|
|
7959
8064
|
const inboxMessageBodySchema = Type.Object({
|
|
7960
8065
|
payload: Type.Optional(Type.Unknown()),
|
|
7961
8066
|
position: Type.Optional(Type.String()),
|
|
@@ -7977,7 +8082,19 @@ const forkBodySchema = Type.Object({
|
|
|
7977
8082
|
fork_pointer: Type.Optional(Type.Object({
|
|
7978
8083
|
offset: Type.Union([Type.String(), Type.Null()]),
|
|
7979
8084
|
sub_offset: Type.Number()
|
|
7980
|
-
}))
|
|
8085
|
+
})),
|
|
8086
|
+
anchor: Type.Optional(Type.Literal(`latest_completed_run`)),
|
|
8087
|
+
parent: Type.Optional(Type.String()),
|
|
8088
|
+
wake: Type.Optional(Type.Object({
|
|
8089
|
+
subscriberUrl: Type.String(),
|
|
8090
|
+
condition: wakeConditionSchema,
|
|
8091
|
+
debounceMs: Type.Optional(Type.Number()),
|
|
8092
|
+
timeoutMs: Type.Optional(Type.Number()),
|
|
8093
|
+
includeResponse: Type.Optional(Type.Boolean()),
|
|
8094
|
+
manifestKey: Type.Optional(Type.String())
|
|
8095
|
+
})),
|
|
8096
|
+
initialMessage: Type.Optional(Type.Unknown()),
|
|
8097
|
+
tags: Type.Optional(stringRecordSchema$1)
|
|
7981
8098
|
});
|
|
7982
8099
|
const setTagBodySchema = Type.Object({ value: Type.String() });
|
|
7983
8100
|
const entitySignalSchema = Type.Union([
|
|
@@ -8364,8 +8481,18 @@ async function forkEntity(request, ctx) {
|
|
|
8364
8481
|
const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
|
|
8365
8482
|
if (principalMutationError) return principalMutationError;
|
|
8366
8483
|
const parsed = routeBody(request);
|
|
8484
|
+
if (parsed.fork_pointer && parsed.anchor) return apiError(400, ErrCodeInvalidRequest, `fork_pointer and anchor are mutually exclusive`);
|
|
8367
8485
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
8368
8486
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
8487
|
+
if (parsed.parent !== void 0) {
|
|
8488
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
8489
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
8490
|
+
if (!await canAccessEntity(ctx, parent, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
8491
|
+
}
|
|
8492
|
+
if (parsed.wake !== void 0) {
|
|
8493
|
+
if (parsed.parent === void 0) return apiError(400, ErrCodeInvalidRequest, `wake requires parent (the fork's wake fires to its parent)`);
|
|
8494
|
+
if (parsed.wake.subscriberUrl !== parsed.parent) return apiError(401, ErrCodeUnauthorized, `wake.subscriberUrl must match parent`);
|
|
8495
|
+
}
|
|
8369
8496
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
8370
8497
|
rootInstanceId: parsed.instance_id,
|
|
8371
8498
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
@@ -8373,9 +8500,17 @@ async function forkEntity(request, ctx) {
|
|
|
8373
8500
|
...parsed.fork_pointer && { forkPointer: {
|
|
8374
8501
|
offset: parsed.fork_pointer.offset,
|
|
8375
8502
|
subOffset: parsed.fork_pointer.sub_offset
|
|
8376
|
-
} }
|
|
8503
|
+
} },
|
|
8504
|
+
...parsed.anchor && { anchor: parsed.anchor },
|
|
8505
|
+
...parsed.parent !== void 0 && { parent: parsed.parent },
|
|
8506
|
+
...parsed.wake !== void 0 && { wake: parsed.wake },
|
|
8507
|
+
...parsed.tags !== void 0 && { tags: parsed.tags }
|
|
8377
8508
|
});
|
|
8378
8509
|
for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
|
|
8510
|
+
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(result.root.url, {
|
|
8511
|
+
from: parsed.parent ?? ctx.principal.url,
|
|
8512
|
+
payload: parsed.initialMessage
|
|
8513
|
+
});
|
|
8379
8514
|
return json({
|
|
8380
8515
|
root: toPublicEntity(result.root),
|
|
8381
8516
|
entities: result.entities.map((entity$1) => toPublicEntity(entity$1))
|
|
@@ -8388,7 +8523,10 @@ async function sendEntity(request, ctx) {
|
|
|
8388
8523
|
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
8389
8524
|
if (parsed.from_agent !== void 0) {
|
|
8390
8525
|
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
8391
|
-
|
|
8526
|
+
const fromAgentUrl = agentUrlPath(parsed.from_agent);
|
|
8527
|
+
const matchesPrincipalAgent = fromAgentUrl === principalAgentUrl;
|
|
8528
|
+
const hasAgentWriteToken = await hasValidAgentWriteToken(request, ctx, parsed.from_agent);
|
|
8529
|
+
if (!matchesPrincipalAgent && !hasAgentWriteToken) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
8392
8530
|
}
|
|
8393
8531
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
8394
8532
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
@@ -8474,6 +8612,7 @@ async function spawnEntity(request, ctx) {
|
|
|
8474
8612
|
dispatch_policy: dispatchPolicy,
|
|
8475
8613
|
sandbox: parsed.sandbox,
|
|
8476
8614
|
initialMessage: void 0,
|
|
8615
|
+
initialMessageType: void 0,
|
|
8477
8616
|
wake: parsed.wake,
|
|
8478
8617
|
created_by: principal.url
|
|
8479
8618
|
});
|
|
@@ -8492,7 +8631,8 @@ async function spawnEntity(request, ctx) {
|
|
|
8492
8631
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
8493
8632
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
8494
8633
|
from: principal.url,
|
|
8495
|
-
payload: parsed.initialMessage
|
|
8634
|
+
payload: parsed.initialMessage,
|
|
8635
|
+
type: parsed.initialMessageType
|
|
8496
8636
|
});
|
|
8497
8637
|
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
8498
8638
|
return json({
|
|
@@ -8539,6 +8679,21 @@ async function signalEntity(request, ctx) {
|
|
|
8539
8679
|
//#region src/routing/entity-types-router.ts
|
|
8540
8680
|
const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
|
|
8541
8681
|
const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
|
|
8682
|
+
const slashCommandArgumentSchema = Type.Object({
|
|
8683
|
+
name: Type.String(),
|
|
8684
|
+
type: Type.Union([
|
|
8685
|
+
Type.Literal(`string`),
|
|
8686
|
+
Type.Literal(`number`),
|
|
8687
|
+
Type.Literal(`boolean`)
|
|
8688
|
+
]),
|
|
8689
|
+
required: Type.Optional(Type.Boolean()),
|
|
8690
|
+
description: Type.Optional(Type.String())
|
|
8691
|
+
}, { additionalProperties: false });
|
|
8692
|
+
const slashCommandSchema = Type.Object({
|
|
8693
|
+
name: Type.String(),
|
|
8694
|
+
description: Type.Optional(Type.String()),
|
|
8695
|
+
arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
|
|
8696
|
+
}, { additionalProperties: false });
|
|
8542
8697
|
const typePermissionGrantInputSchema = Type.Object({
|
|
8543
8698
|
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
8544
8699
|
subject_value: Type.String(),
|
|
@@ -8551,6 +8706,7 @@ const registerEntityTypeBodySchema = Type.Object({
|
|
|
8551
8706
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
8552
8707
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
8553
8708
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
8709
|
+
slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
|
|
8554
8710
|
serve_endpoint: Type.Optional(Type.String()),
|
|
8555
8711
|
default_dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
8556
8712
|
permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
|
|
@@ -8692,6 +8848,7 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
8692
8848
|
creation_schema: parsed.creation_schema,
|
|
8693
8849
|
inbox_schemas: parsed.inbox_schemas,
|
|
8694
8850
|
state_schemas: parsed.state_schemas,
|
|
8851
|
+
slash_commands: parsed.slash_commands,
|
|
8695
8852
|
serve_endpoint: serveEndpoint,
|
|
8696
8853
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
8697
8854
|
type: `webhook`,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "entity_types" ADD COLUMN "slash_commands" jsonb;
|
|
@@ -99,6 +99,13 @@
|
|
|
99
99
|
"when": 1780588695000,
|
|
100
100
|
"tag": "0013_worker_user_manage_permission",
|
|
101
101
|
"breakpoints": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"idx": 14,
|
|
105
|
+
"version": "7",
|
|
106
|
+
"when": 1780600000000,
|
|
107
|
+
"tag": "0014_entity_type_slash_commands",
|
|
108
|
+
"breakpoints": true
|
|
102
109
|
}
|
|
103
110
|
]
|
|
104
111
|
}
|
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.18",
|
|
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.3.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.3.11"
|
|
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": "0.4.
|
|
69
|
-
"@electric-ax/agents
|
|
70
|
-
"@electric-ax/agents-server-
|
|
68
|
+
"@electric-ax/agents-server-ui": "0.4.18",
|
|
69
|
+
"@electric-ax/agents": "0.4.15",
|
|
70
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.11"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
package/src/db/schema.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const entityTypes = pgTable(
|
|
|
24
24
|
creationSchema: jsonb(`creation_schema`),
|
|
25
25
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
26
26
|
stateSchemas: jsonb(`state_schemas`),
|
|
27
|
+
slashCommands: jsonb(`slash_commands`),
|
|
27
28
|
serveEndpoint: text(`serve_endpoint`),
|
|
28
29
|
defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
|
|
29
30
|
revision: integer(`revision`).notNull().default(1),
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
PullWakeRunnerHealth,
|
|
7
|
+
SlashCommandDefinition,
|
|
7
8
|
WebhookNotification,
|
|
8
9
|
} from '@electric-ax/agents-runtime'
|
|
9
10
|
import type { Principal } from './principal.js'
|
|
@@ -499,6 +500,7 @@ export interface ElectricAgentsEntityType {
|
|
|
499
500
|
creation_schema?: Record<string, unknown>
|
|
500
501
|
inbox_schemas?: Record<string, Record<string, unknown>>
|
|
501
502
|
state_schemas?: Record<string, Record<string, unknown>>
|
|
503
|
+
slash_commands?: Array<SlashCommandDefinition>
|
|
502
504
|
serve_endpoint?: string
|
|
503
505
|
default_dispatch_policy?: DispatchPolicy
|
|
504
506
|
revision: number
|
|
@@ -512,6 +514,7 @@ export interface RegisterEntityTypeRequest {
|
|
|
512
514
|
creation_schema?: Record<string, unknown>
|
|
513
515
|
inbox_schemas?: Record<string, Record<string, unknown>>
|
|
514
516
|
state_schemas?: Record<string, Record<string, unknown>>
|
|
517
|
+
slash_commands?: Array<SlashCommandDefinition>
|
|
515
518
|
serve_endpoint?: string
|
|
516
519
|
default_dispatch_policy?: DispatchPolicy
|
|
517
520
|
permission_grants?: Array<EntityTypePermissionGrantInput>
|
|
@@ -530,6 +533,7 @@ export interface TypedSpawnRequest {
|
|
|
530
533
|
*/
|
|
531
534
|
sandbox?: SandboxChoice
|
|
532
535
|
initialMessage?: unknown
|
|
536
|
+
initialMessageType?: string
|
|
533
537
|
created_by?: string
|
|
534
538
|
wake?: {
|
|
535
539
|
subscriberUrl: string
|