@segmentstream-cloud/ontology 0.14.0 → 0.15.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 (37) hide show
  1. package/README.md +35 -10
  2. package/dist/agent-chat.d.ts +75 -0
  3. package/dist/agent-chat.js +176 -0
  4. package/dist/analyze.d.ts +6 -2
  5. package/dist/analyze.js +29 -18
  6. package/dist/authoring.d.ts +6 -3
  7. package/dist/authoring.js +6 -3
  8. package/dist/index.d.ts +4 -2
  9. package/dist/index.js +2 -1
  10. package/dist/permissions.d.ts +11 -2
  11. package/dist/permissions.js +15 -3
  12. package/dist/replication/baseline.js +1 -1
  13. package/dist/replication/model.js +2 -2
  14. package/dist/replication/schema.js +1 -1
  15. package/dist/runtime/agents.d.ts +1 -0
  16. package/dist/runtime/agents.js +160 -0
  17. package/dist/runtime/generated-actions.d.ts +2 -1
  18. package/dist/runtime/generated-actions.js +18 -13
  19. package/dist/service/agent/channel.d.ts +16 -0
  20. package/dist/service/agent/channel.js +88 -0
  21. package/dist/service/agent/protocol.d.ts +14 -0
  22. package/dist/service/agent/protocol.js +26 -0
  23. package/dist/service/agent/server.d.ts +2 -1
  24. package/dist/service/agent/server.js +24 -2
  25. package/dist/service/aggregate-platform.d.ts +5 -0
  26. package/dist/service/aggregate-platform.js +1 -1
  27. package/dist/service/config.d.ts +10 -0
  28. package/dist/service/config.js +8 -0
  29. package/dist/service/handlers.d.ts +2 -1
  30. package/dist/service/handlers.js +2 -2
  31. package/dist/service/mcp/server.d.ts +3 -0
  32. package/dist/service/mcp/server.js +1 -1
  33. package/dist/service/operations.d.ts +6 -2
  34. package/dist/service/operations.js +13 -5
  35. package/dist/service/runtime-contract.d.ts +26 -0
  36. package/dist/service/service.js +7 -1
  37. package/package.json +1 -1
@@ -34,6 +34,11 @@ export const RUNTIME_IDENTITY_ENVIRONMENT_VARIABLES = {
34
34
  audience: "SEGMENTSTREAM_RUNTIME_OIDC_AUDIENCE",
35
35
  projectId: "VERCEL_PROJECT_ID",
36
36
  };
37
+ /**
38
+ * The project's production domain, which Vercel sets: the agent of a deployment with runtime identity
39
+ * takes the messages of `agent_chat.send_message` there.
40
+ */
41
+ export const AGENT_HOST_VARIABLE = "VERCEL_PROJECT_PRODUCTION_URL";
37
42
  /** The service cannot start: its configuration is incomplete or invalid. */
38
43
  export class OntologyServiceConfigError extends Schema.TaggedError()("OntologyServiceConfigError", { message: Schema.String }) {
39
44
  }
@@ -160,6 +165,9 @@ export function configFromEnv(environment = process.env) {
160
165
  : {}),
161
166
  ...(neonAuth === undefined ? {} : { neonAuth }),
162
167
  ...(runtimeIdentity === undefined ? {} : { runtimeIdentity }),
168
+ ...(runtimeIdentity === undefined || read(AGENT_HOST_VARIABLE) === ""
169
+ ? {}
170
+ : { agentUrl: normalizeUrl(AGENT_HOST_VARIABLE, `https://${read(AGENT_HOST_VARIABLE)}`) }),
163
171
  ...(localConnections === undefined ? {} : { localConnections }),
164
172
  };
165
173
  }
@@ -4,6 +4,7 @@ import type { Aggregates } from "./aggregate.js";
4
4
  import { type BuiltApi } from "./api.js";
5
5
  import type { AfterCommitHook } from "./config.js";
6
6
  import type { ReplicationKick } from "./replication.js";
7
+ import type { AgentChannelFactory } from "./agent/channel.js";
7
8
  import type { ConnectionTransportFactory } from "./connections.js";
8
9
  import { type RateLimit } from "./rate-limit.js";
9
10
  import { OntologyBackend } from "./runtime-contract.js";
@@ -13,4 +14,4 @@ import { OntologyBackend } from "./runtime-contract.js";
13
14
  */
14
15
  export declare const SIGN_IN_CHECK_LIMIT: RateLimit;
15
16
  /** The handlers of every group of the API; they require {@link OntologyBackend}. */
16
- export declare function handlersLayer(built: BuiltApi, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory, replicate: ReplicationKick, aggregates: Aggregates): Layer.Layer<HttpApiGroup.Service<"ontology", string>, never, OntologyBackend>;
17
+ export declare function handlersLayer(built: BuiltApi, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory, replicate: ReplicationKick, aggregates: Aggregates, agentChannel: AgentChannelFactory | undefined): Layer.Layer<HttpApiGroup.Service<"ontology", string>, never, OntologyBackend>;
@@ -216,8 +216,8 @@ function storageHandlers(built) {
216
216
  }));
217
217
  }
218
218
  /** The handlers of every group of the API; they require {@link OntologyBackend}. */
219
- export function handlersLayer(built, hooks, connections, replicate, aggregates) {
220
- const operationsOf = (backend) => makeOperations(backend, built.plan, hooks, connections, replicate);
219
+ export function handlersLayer(built, hooks, connections, replicate, aggregates, agentChannel) {
220
+ const operationsOf = (backend) => makeOperations(backend, built.plan, hooks, connections, replicate, agentChannel);
221
221
  const layers = [
222
222
  serviceHandlers(built, operationsOf, aggregates.model),
223
223
  ...(built.plan.ontology.manifest.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))
@@ -4,6 +4,7 @@ import type { Aggregates } from "../aggregate.js";
4
4
  import type { BuiltApi } from "../api.js";
5
5
  import { type TokenVerifier } from "../authentication.js";
6
6
  import type { AfterCommitHook } from "../config.js";
7
+ import type { AgentChannelFactory } from "../agent/channel.js";
7
8
  import type { ConnectionTransportFactory } from "../connections.js";
8
9
  import { RequestId } from "../errors.js";
9
10
  import type { ReplicationKick } from "../replication.js";
@@ -23,6 +24,8 @@ export interface McpServerOptions {
23
24
  readonly verifyRuntime?: RuntimeTokenVerifier | undefined;
24
25
  /** The facade the aggregate routes run; the aggregate tool runs its model-level aggregate. */
25
26
  readonly aggregates: Aggregates;
27
+ /** The workspace's agent, for `agent_chat.send_message`; `undefined` without one. */
28
+ readonly agentChannel?: AgentChannelFactory | undefined;
26
29
  }
27
30
  /**
28
31
  * The ontology's MCP server at `POST /mcp`: Streamable HTTP without sessions, so any instance can
@@ -417,7 +417,7 @@ export function mcpLayer(options) {
417
417
  const server = {
418
418
  built: options.built,
419
419
  tools,
420
- operations: makeOperations(backend, options.built.plan, options.hooks, options.connections, options.replicate),
420
+ operations: makeOperations(backend, options.built.plan, options.hooks, options.connections, options.replicate, options.agentChannel),
421
421
  aggregate: options.aggregates.model,
422
422
  };
423
423
  yield* router.add("POST", MCP_PATH, handlePost(server, options.verifyToken, options.verifyRuntime, backend));
@@ -5,6 +5,7 @@ import type { Query, QueryResult } from "../query.js";
5
5
  import { CurrentPrincipal } from "./authentication.js";
6
6
  import type { AfterCommitHook } from "./config.js";
7
7
  import type { ReplicationKick } from "./replication.js";
8
+ import type { AgentChannelFactory } from "./agent/channel.js";
8
9
  import { type ConnectionTransportFactory } from "./connections.js";
9
10
  import { InvalidActionInput, RequestId } from "./errors.js";
10
11
  import type { ApiPlan, PlannedAction, PlannedEntity, Traversal } from "./plan.js";
@@ -59,8 +60,11 @@ export interface Caller {
59
60
  readonly permissions: ReadonlyArray<string>;
60
61
  }
61
62
  type Backend = OntologyBackend["Service"];
62
- /** The operations of a planned ontology, bound to its backend, after-commit hooks and connections. */
63
- export declare function makeOperations(backend: Backend, plan: ApiPlan, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory, replicate: ReplicationKick): {
63
+ /**
64
+ * The operations of a planned ontology, bound to its backend, after-commit hooks, connections and,
65
+ * on deployments with one, the workspace's agent.
66
+ */
67
+ export declare function makeOperations(backend: Backend, plan: ApiPlan, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory, replicate: ReplicationKick, agentChannel?: AgentChannelFactory): {
64
68
  /** A page of records. Requires `read` on the entity and on every entity the query reaches. */
65
69
  find: (planned: PlannedEntity, query: Query) => Effect.Effect<QueryResult<Readonly<Record<string, unknown>>>, PermissionDenied | import("../index.js").InvalidQuery, CurrentPrincipal>;
66
70
  /** One record. Requires `read` on the entity. */
@@ -6,6 +6,7 @@ import { PermissionDenied } from "../errors.js";
6
6
  import { toCamelCase } from "../names.js";
7
7
  import { missingPermission, permissionOf, READ_VERB } from "../permissions.js";
8
8
  import { CurrentPrincipal } from "./authentication.js";
9
+ import { AgentChannel } from "../agent-chat.js";
9
10
  import { OIDC_TOKEN_HEADER } from "./connections.js";
10
11
  import { InvalidActionInput, RequestId } from "./errors.js";
11
12
  /**
@@ -145,12 +146,12 @@ function traverseOf(backend, targets, planned, traversal, id, query) {
145
146
  return page(yield* backend.find({ entity: traversal.target, query: { ...query, where } }));
146
147
  });
147
148
  }
148
- function runActionOf(backend, ontology, hooks, connections, replicate) {
149
+ function runActionOf(backend, ontology, hooks, connections, replicate, agentChannel) {
149
150
  return (planned, { name, generated }, call) => Effect.gen(function* () {
150
151
  const principal = yield* CurrentPrincipal;
151
152
  const requestId = yield* RequestId;
152
153
  const hook = hooks.get(`${planned.name}.${name}`);
153
- const outcome = yield* backend.runAction({
154
+ const run = backend.runAction({
154
155
  entity: planned.name,
155
156
  action: name,
156
157
  input: call.input,
@@ -175,6 +176,10 @@ function runActionOf(backend, ontology, hooks, connections, replicate) {
175
176
  }
176
177
  : {}),
177
178
  });
179
+ // Built-in actions reach the workspace's agent through it, when the deployment has one.
180
+ const outcome = yield* agentChannel === undefined
181
+ ? run
182
+ : run.pipe(Effect.provideService(AgentChannel, agentChannel({ identityToken: call.identityToken, store: backend.agents })));
178
183
  const { issues } = outcome.validation;
179
184
  if (!outcome.validation.ok && !call.validateOnly) {
180
185
  return yield* new InvalidActionInput({ issues });
@@ -208,8 +213,11 @@ function callerOf(backend) {
208
213
  };
209
214
  });
210
215
  }
211
- /** The operations of a planned ontology, bound to its backend, after-commit hooks and connections. */
212
- export function makeOperations(backend, plan, hooks, connections, replicate) {
216
+ /**
217
+ * The operations of a planned ontology, bound to its backend, after-commit hooks, connections and,
218
+ * on deployments with one, the workspace's agent.
219
+ */
220
+ export function makeOperations(backend, plan, hooks, connections, replicate, agentChannel) {
213
221
  const targets = relationshipTargets(plan);
214
222
  return {
215
223
  /** A page of records. Requires `read` on the entity and on every entity the query reaches. */
@@ -221,7 +229,7 @@ export function makeOperations(backend, plan, hooks, connections, replicate) {
221
229
  /** The records a relationship reaches from one record, as a page of the target entity. */
222
230
  traverse: (planned, traversal, id, query) => traverseOf(backend, targets, planned, traversal, id, query),
223
231
  /** Runs an action as the caller, with the action's after-commit hook and connections. */
224
- runAction: runActionOf(backend, plan.ontology, hooks, connections, replicate),
232
+ runAction: runActionOf(backend, plan.ontology, hooks, connections, replicate, agentChannel),
225
233
  /** The caller's user, roles and permissions. It needs no permission. */
226
234
  caller: callerOf(backend),
227
235
  };
@@ -180,6 +180,13 @@ export interface AgentMember {
180
180
  readonly name: string;
181
181
  readonly principal: Principal;
182
182
  }
183
+ /** A group chat the agent saw, as `POST /api/ontology/agent/chats/seen` answers. */
184
+ export interface SeenChat {
185
+ readonly id: string;
186
+ readonly title: string;
187
+ readonly purpose: string | null;
188
+ readonly whenToUse: string | null;
189
+ }
183
190
  /**
184
191
  * Where the app's agent keeps pairing codes and the identities they create: the runtime's agent
185
192
  * tables in the workspace database. Codes are stored only as SHA-256 hashes. Expiry is checked
@@ -218,6 +225,25 @@ export interface AgentStore {
218
225
  readonly member: (userId: string) => Effect.Effect<AgentMember | undefined>;
219
226
  /** The member a runtime caller acts for: active, and with a live identity. */
220
227
  readonly actingMember: (userId: string) => Effect.Effect<Principal | undefined>;
228
+ /**
229
+ * Records a message the agent saw in a group chat: creates the `agent_chat` or keeps its title
230
+ * current, records when the chat last had a message (at most every 15 minutes, so that history
231
+ * keeps it without a row per message), links the writer to its members when they are an active
232
+ * member, and unlinks `leftMemberId`, a member who left, when linked. With `agentLeft`, the agent
233
+ * itself left the chat: every member is unlinked, and the chat stays. With `migratedFrom`, the
234
+ * chat had that id before it became a supergroup: its record moves to `externalId`, or, when both
235
+ * ids have one, the new record takes the old one's members, and its purpose and when to use where
236
+ * it has none. Concurrent calls for one chat leave one record.
237
+ */
238
+ readonly seeChat: (input: {
239
+ readonly provider: string;
240
+ readonly externalId: string;
241
+ readonly title: string;
242
+ readonly memberId?: string;
243
+ readonly leftMemberId?: string;
244
+ readonly agentLeft?: true;
245
+ readonly migratedFrom?: string;
246
+ }) => Effect.Effect<SeenChat>;
221
247
  }
222
248
  declare const OntologyBackend_base: Context.ServiceClass<OntologyBackend, "@segmentstream-cloud/ontology/OntologyBackend", {
223
249
  readonly blobs?: BlobRuntime;
@@ -4,6 +4,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi";
4
4
  import { storageMaintenanceAuthenticationLayer } from "../storage/maintenance-auth.js";
5
5
  import { nameToWire } from "../authoring-names.js";
6
6
  import { sqlServiceRuntime } from "../runtime/service-binding.js";
7
+ import { deployedAgentChannel } from "./agent/channel.js";
7
8
  import { agentLayer } from "./agent/server.js";
8
9
  import { makeAggregates } from "./aggregate.js";
9
10
  import { platformAggregates } from "./aggregate-platform.js";
@@ -89,11 +90,15 @@ export function createOntologyService(ontology, config) {
89
90
  ? deployedConnections(config)
90
91
  : localConnections(config.localConnections));
91
92
  const replicate = deployedReplication(config);
93
+ // The agent's runtime identity is configured only on deployments that have an agent.
94
+ const agentChannel = config.runtimeIdentity === undefined || config.agentUrl === undefined
95
+ ? undefined
96
+ : deployedAgentChannel(config.agentUrl);
92
97
  const aggregates = makeAggregates({
93
98
  manifest: ontology.manifest,
94
99
  transport: platformAggregates({ ...config, manifestHash: ontology.manifest.hash }),
95
100
  });
96
- const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate, aggregates)), Layer.provide([
101
+ const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate, aggregates, agentChannel)), Layer.provide([
97
102
  authenticationLayer(verifyToken, exchangeSession, verifyRuntime),
98
103
  ErrorResponsesLive,
99
104
  storageMaintenanceAuthenticationLayer(config.storageMaintenanceKey),
@@ -108,6 +113,7 @@ export function createOntologyService(ontology, config) {
108
113
  verifyRuntime,
109
114
  replicate,
110
115
  aggregates,
116
+ agentChannel,
111
117
  }), oauthLayer({ verifyToken }));
112
118
  const agent = agentLayer({ verifyToken, verifyRuntime, exchangeSession });
113
119
  const application = Layer.mergeAll(routes, mcp, agent, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/ontology",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",