@segmentstream-cloud/ontology 0.19.0 → 0.21.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.
package/README.md CHANGED
@@ -370,7 +370,7 @@ both are recorded, moves the members and a missing `purpose` and `whenToUse` to
370
370
  deletes the old one. `provider`, `externalId`, `title` and `lastMessageAt` are
371
371
  managed. Members set `purpose` and `whenToUse` with the generated
372
372
  `agent_chat.update`, the chat's only generated action. The built-in action
373
- `agent_chat.send_message` (`{ message, chat?, person? }`, `message` at most 4000 characters) asks the agent to write to the chat
373
+ `agent_chat.send_message` (`{ message, chat?, person?, document_ids? }`, `message` at most 4000 characters) asks the agent to write to the chat
374
374
  given, else to the person's one chat, else to their paired private chat; a person in several
375
375
  chats is refused with the candidates, or only their number without `agent_chat:read`. The service posts the message to the agent at
376
376
  `https://<VERCEL_PROJECT_PRODUCTION_URL>/eve/v1/telegram/messages` with the deployment's OIDC
@@ -382,6 +382,30 @@ chats needs `agent_chat:read` by name: `*:read` and `*:*` do not cover `agent_ch
382
382
 
383
383
  ### Agent attachments
384
384
 
385
+ Ontology SDK 0.21.0 adds `documentIds` to the authored action input (`document_ids` on REST/MCP):
386
+ an optional array of at most ten `document` record UUIDs. Before posting anything to the agent,
387
+ the action checks `document:read`, every record's existence and its single file property.
388
+ Missing files refuse the request; unreadable files require the read permission; ambiguous file
389
+ properties are refused. Requests without the field retain their previous behavior. The service
390
+ requires a paired member requester for requests with documents, since the agent downloads as
391
+ that member; services and unpaired requesters may still send requests without documents. It
392
+ forwards `documentIds` to `POST /eve/v1/telegram/messages`; agent SDK 0.5.0 includes the ids in
393
+ `<agent_request>` and sends the stored bytes with `segmentstream__attachments_send`.
394
+
395
+ `GET /api/ontology/agent/files/:entity/:id[/:property]` returns `{ file, download }` for the
396
+ current file property of an ontology record. Like temporary attachment routes, it requires a
397
+ production runtime bearer token, `X-SegmentStream-Act-As` naming an active paired member,
398
+ and the agent conversation/session headers. It checks `<entity>:read` before returning metadata
399
+ and rechecks live permissions when granting a download pinned to that revision. The response is
400
+ uncached; the download grant is short lived. Without a property the entity must have exactly one
401
+ file property. Missing records/files return `not_found`, missing read access `forbidden`, and
402
+ non-file or ambiguous properties `unsupported`.
403
+
404
+ Outbound Telegram uploads currently have an effective **2 MiB multipart request limit** in the
405
+ connection proxy, including overhead, even though Telegram accepts larger files. The agent splits
406
+ compatible albums to fit and returns per-file errors. Group purpose and document restrictions
407
+ still apply; a member's read access alone does not authorize sharing a restricted document in a group.
408
+
385
409
  A file a member sends the agent is a temporary attachment: platform metadata in the runtime
386
410
  table `_agent_attachments`, never an ontology record, with its bytes stored once as a revision in
387
411
  the ontology's file storage, beside uploads. It belongs to the member, the chat it arrived in and
@@ -14,6 +14,8 @@ export interface AgentMessage {
14
14
  };
15
15
  /** The request for the agent, written as an instruction to it. */
16
16
  readonly message: string;
17
+ /** Document record ids to send with the request; absent from older requests. */
18
+ readonly documentIds?: ReadonlyArray<string>;
17
19
  /** Whom the agent should address, when the caller named someone. */
18
20
  readonly person: {
19
21
  readonly id: string;
@@ -63,6 +65,7 @@ export declare const AgentChat: import("./entity.js").Entity<"agent_chat", {
63
65
  }, {
64
66
  readonly sendMessage: import("./action.js").Action<Schema.Struct<{
65
67
  readonly message: import("./property.js").Property<string, false, false>;
68
+ readonly documentIds: Schema.optionalKey<Schema.$Array<Schema.String>>;
66
69
  readonly chat: Schema.optionalKey<Schema.Codec<string, string, never, never>>;
67
70
  readonly person: Schema.optionalKey<Schema.Codec<string, string, never, never>>;
68
71
  }>, {
@@ -6,6 +6,7 @@ import { isAllowed, permissionOf, READ_VERB } from "./permissions.js";
6
6
  import { p } from "./property.js";
7
7
  import { rel } from "./relationship.js";
8
8
  import { User } from "./user.js";
9
+ import { AgentFileRecords } from "./runtime/files.js";
9
10
  /** The only channel the agent delivers to so far, and the provider of its chats. */
10
11
  export const TELEGRAM = "telegram";
11
12
  /**
@@ -84,12 +85,16 @@ export const AgentChat = entity("agent_chat", {
84
85
  "chats is refused with the candidates (id, title, purpose, when to use; only their number " +
85
86
  "without agent_chat:read): call again with chat. The agent then writes in that chat and " +
86
87
  "handles the replies there, in that chat's own session; this action only delivers the " +
87
- "request and does not wait for an answer. Requires an agent deployed in the workspace.",
88
+ "request and does not wait for an answer. document_ids optionally names up to ten readable " +
89
+ "document records to attach with the question. Requires an agent deployed in the workspace.",
88
90
  input: Schema.Struct({
89
91
  message: p.text({
90
92
  maxLength: 4000,
91
93
  description: "The request for the agent, as an instruction: what to ask or tell, of whom.",
92
94
  }),
95
+ documentIds: Schema.optionalKey(Schema.Array(Schema.String.check(Schema.isUUID()))
96
+ .check(Schema.isMaxLength(10))
97
+ .annotate({ description: "Up to ten document record ids the requester may read." })),
93
98
  chat: p.ref("agent_chat", { optional: true, description: "The agent chat to write in." }),
94
99
  person: p.ref("user", {
95
100
  optional: true,
@@ -103,6 +108,19 @@ export const AgentChat = entity("agent_chat", {
103
108
  if (Option.isNone(channel)) {
104
109
  return yield* new ActionRejected({ message: NO_AGENT });
105
110
  }
111
+ if (input.documentIds !== undefined && input.documentIds.length > 0) {
112
+ const files = yield* Effect.serviceOption(AgentFileRecords);
113
+ if (Option.isNone(files)) {
114
+ return yield* new ActionRejected({
115
+ message: "This runtime cannot validate stored documents.",
116
+ });
117
+ }
118
+ for (const id of input.documentIds) {
119
+ yield* files.value
120
+ .resolve({ entity: "document", id }, principal)
121
+ .pipe(Effect.catchTag("UnsupportedFile", (error) => Effect.fail(new ActionRejected({ message: error.message }))));
122
+ }
123
+ }
106
124
  const person = input.person === undefined ? undefined : yield* User.get(input.person);
107
125
  if (person !== undefined && (person.kind !== "member" || !person.active)) {
108
126
  return yield* new ActionRejected({
@@ -158,9 +176,15 @@ export const AgentChat = entity("agent_chat", {
158
176
  // The agent acts as a member only through their pairing.
159
177
  const actAs = principal.kind === "member" &&
160
178
  (yield* channel.value.privateChat(principal.id)) !== undefined;
179
+ if ((input.documentIds?.length ?? 0) > 0 && !actAs) {
180
+ return yield* new ActionRejected({
181
+ message: "Sending documents requires a member requester with a paired Telegram account.",
182
+ });
183
+ }
161
184
  yield* channel.value.send({
162
185
  target,
163
186
  message: input.message,
187
+ ...(input.documentIds === undefined ? {} : { documentIds: input.documentIds }),
164
188
  person: person === undefined ? null : { id: person.id, name: person.name },
165
189
  requester: {
166
190
  id: requester.id,
@@ -0,0 +1,39 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+ import { NotFound, PermissionDenied } from "../errors.js";
3
+ import type { OntologyManifest } from "../manifest.js";
4
+ import type { OntologyReads, Principal } from "../runtime.js";
5
+ export interface FileReference {
6
+ readonly entity: string;
7
+ readonly id: string;
8
+ readonly property?: string;
9
+ }
10
+ declare const UnsupportedFile_base: Schema.Class<UnsupportedFile, Schema.TaggedStruct<"UnsupportedFile", {
11
+ readonly message: Schema.String;
12
+ }>, import("effect/Cause").YieldableError>;
13
+ export declare class UnsupportedFile extends UnsupportedFile_base {
14
+ }
15
+ /** Resolve only declared file properties, checking permission before reading any record. */
16
+ export declare function resolveFileRecord(input: {
17
+ readonly manifest: OntologyManifest;
18
+ readonly reads: Pick<OntologyReads["Service"], "get">;
19
+ readonly principal: Principal;
20
+ readonly reference: FileReference;
21
+ }): Effect.Effect<{
22
+ entity: string;
23
+ id: string;
24
+ property: string;
25
+ file: {
26
+ readonly id: string;
27
+ readonly filename: string;
28
+ readonly contentType: string;
29
+ readonly size: number;
30
+ readonly createdAt: string;
31
+ };
32
+ }, PermissionDenied | NotFound | UnsupportedFile, never>;
33
+ declare const AgentFileRecords_base: Context.ServiceClass<AgentFileRecords, "@segmentstream-cloud/ontology/AgentFileRecords", {
34
+ readonly resolve: (reference: FileReference, principal: Principal) => ReturnType<typeof resolveFileRecord>;
35
+ }>;
36
+ /** File validation in an action's prepare, bound to its ontology and read runtime. */
37
+ export declare class AgentFileRecords extends AgentFileRecords_base {
38
+ }
39
+ export {};
@@ -0,0 +1,44 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+ import { BlobValue } from "../blob.js";
3
+ import { NotFound, PermissionDenied } from "../errors.js";
4
+ import { isAllowed, permissionOf, READ_VERB } from "../permissions.js";
5
+ export class UnsupportedFile extends Schema.TaggedError()("UnsupportedFile", {
6
+ message: Schema.String,
7
+ }) {
8
+ }
9
+ /** Resolve only declared file properties, checking permission before reading any record. */
10
+ export function resolveFileRecord(input) {
11
+ return Effect.gen(function* () {
12
+ const { entity: entityName, id, property: propertyName } = input.reference;
13
+ const permission = permissionOf(entityName, READ_VERB);
14
+ if (!isAllowed(permission, input.principal)) {
15
+ return yield* new PermissionDenied({ permission, message: "The file requires read access." });
16
+ }
17
+ const entity = input.manifest.entities.find((candidate) => candidate.name === entityName);
18
+ if (entity === undefined) {
19
+ return yield* new NotFound({ entity: entityName, id, message: "File record not found." });
20
+ }
21
+ const properties = entity.properties.filter((candidate) => candidate.type === "blob");
22
+ const property = propertyName === undefined
23
+ ? properties.length === 1
24
+ ? properties[0]
25
+ : undefined
26
+ : properties.find((candidate) => candidate.name === propertyName);
27
+ if (property === undefined) {
28
+ return yield* new UnsupportedFile({
29
+ message: "Name a file property; the entity must have exactly one when property is omitted.",
30
+ });
31
+ }
32
+ const record = yield* input.reads.get({ entity: entityName, id });
33
+ const file = record[property.name];
34
+ if (file === null || file === undefined) {
35
+ return yield* new NotFound({ entity: entityName, id, message: "File not found." });
36
+ }
37
+ // Runtime records are decoded already; malformed stored metadata is an operator failure.
38
+ const decoded = yield* Schema.decodeUnknownEffect(BlobValue)(file).pipe(Effect.orDie);
39
+ return { entity: entityName, id, property: property.name, file: decoded };
40
+ });
41
+ }
42
+ /** File validation in an action's prepare, bound to its ontology and read runtime. */
43
+ export class AgentFileRecords extends Context.Service()("@segmentstream-cloud/ontology/AgentFileRecords") {
44
+ }
@@ -14,6 +14,7 @@ import { UnknownAction, ValidationIssue } from "./errors.js";
14
14
  import { generatedActions } from "./generated-actions.js";
15
15
  import { Edit } from "./history.js";
16
16
  import { makeOntologyRuntime } from "./sql-runtime.js";
17
+ import { AgentFileRecords, resolveFileRecord } from "./files.js";
17
18
  import { validate } from "./validation.js";
18
19
  /** How long `prepare` may take, connection calls included; each call has its own 30 s deadline. */
19
20
  export const PREPARE_TIMEOUT = Duration.seconds(60);
@@ -243,7 +244,9 @@ function prepare(ontology, sql, action, context, connections, qualified) {
243
244
  find: ({ entity, options }) => runtime.find({ entity, options: { ...options, forUpdate: false } }),
244
245
  };
245
246
  const run = Effect.suspend(() => action.prepare(context));
246
- return yield* run.pipe(Effect.provideService(OntologyReads, reads), Effect.provideContext(connectionContext(action.connections, transport ?? NO_CONNECTIONS)), Effect.timeoutOrElse({
247
+ return yield* run.pipe(Effect.provideService(OntologyReads, reads), Effect.provideService(AgentFileRecords, {
248
+ resolve: (reference, principal) => resolveFileRecord({ manifest: ontology.manifest, reads, reference, principal }),
249
+ }), Effect.provideContext(connectionContext(action.connections, transport ?? NO_CONNECTIONS)), Effect.timeoutOrElse({
247
250
  duration: PREPARE_TIMEOUT,
248
251
  orElse: () => Effect.fail(new ConnectionError({
249
252
  connection: action.connections.map((handle) => handle.key).join(","),
@@ -7,8 +7,9 @@ import { RequestId } from "../errors.js";
7
7
  import type { ApiPlan } from "../plan.js";
8
8
  import type { ReplicationKick } from "../replication.js";
9
9
  import { OntologyBackend } from "../runtime-contract.js";
10
- import { type RuntimeTokenVerifier } from "../runtime-identity.js";
10
+ import type { RuntimeTokenVerifier } from "../runtime-identity.js";
11
11
  import type { AgentChannelFactory } from "./channel.js";
12
+ export { CONVERSATION_HEADER, SESSION_HEADER } from "./authentication.js";
12
13
  /**
13
14
  * The agent's temporary attachments over HTTP (`docs/agent.md`, "Attachments"). Only the
14
15
  * deployment's runtime token reaches these routes, acting as the member in `X-SegmentStream-Act-As`
@@ -17,10 +18,6 @@ import type { AgentChannelFactory } from "./channel.js";
17
18
  * tool arguments; every operation is confined to that member, chat and session.
18
19
  */
19
20
  export declare const ATTACHMENTS_PATH = "/api/ontology/agent/attachments";
20
- /** The chat an attachment belongs to, such as `telegram:12345`. */
21
- export declare const CONVERSATION_HEADER = "x-segmentstream-agent-conversation";
22
- /** The agent session the request comes from. */
23
- export declare const SESSION_HEADER = "x-segmentstream-agent-session";
24
21
  export interface AttachmentRoutesOptions {
25
22
  readonly plan: ApiPlan;
26
23
  readonly hooks: ReadonlyMap<string, AfterCommitHook>;
@@ -1,14 +1,14 @@
1
- import { Cause, Effect, Layer, Redacted, Result, Schema } from "effect";
2
- import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
3
- import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi";
1
+ import { Cause, Effect, Layer, Result, Schema } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
4
3
  import { ATTACHMENT_SOURCES, } from "../../storage/attachments.js";
5
4
  import { schemaIssues } from "../../runtime/validation.js";
6
5
  import { CurrentPrincipal } from "../authentication.js";
7
- import { RequestId, errorBodyForWire, failureResponse, forbidden, toServerResponse, unauthorized, unavailable, } from "../errors.js";
6
+ import { RequestId, errorBodyForWire, failureResponse } from "../errors.js";
8
7
  import { identityToken, makeOperations } from "../operations.js";
9
8
  import { OntologyBackend } from "../runtime-contract.js";
10
- import { ACT_AS_HEADER, logRuntimeRejection, } from "../runtime-identity.js";
9
+ import { authenticateAgentCaller } from "./authentication.js";
11
10
  import { AGENT_PATH } from "./server.js";
11
+ export { CONVERSATION_HEADER, SESSION_HEADER } from "./authentication.js";
12
12
  /**
13
13
  * The agent's temporary attachments over HTTP (`docs/agent.md`, "Attachments"). Only the
14
14
  * deployment's runtime token reaches these routes, acting as the member in `X-SegmentStream-Act-As`
@@ -17,12 +17,6 @@ import { AGENT_PATH } from "./server.js";
17
17
  * tool arguments; every operation is confined to that member, chat and session.
18
18
  */
19
19
  export const ATTACHMENTS_PATH = `${AGENT_PATH}/attachments`;
20
- /** The chat an attachment belongs to, such as `telegram:12345`. */
21
- export const CONVERSATION_HEADER = "x-segmentstream-agent-conversation";
22
- /** The agent session the request comes from. */
23
- export const SESSION_HEADER = "x-segmentstream-agent-session";
24
- const CONVERSATION = /^[a-z][a-z0-9_-]{0,31}:[A-Za-z0-9:_-]{1,200}$/;
25
- const SESSION = /^[A-Za-z0-9:_.-]{1,200}$/;
26
20
  const NO_STORE = { "cache-control": "no-store" };
27
21
  const Received = Schema.Struct({
28
22
  source: Schema.Literals(ATTACHMENT_SOURCES),
@@ -111,39 +105,16 @@ export function attachmentsLayer(options) {
111
105
  return;
112
106
  }
113
107
  const operations = makeOperations(backend, options.plan, options.hooks, options.connections, options.replicate, options.agentChannel);
114
- const credential = HttpApiBuilder.securityDecode(HttpApiSecurity.bearer);
115
108
  /** Authenticates the runtime, then the member it acts for, the chat and the session. */
116
109
  const asAgent = (answer) => (request) => Effect.gen(function* () {
117
- const requestId = yield* RequestId;
118
- const bearer = yield* Effect.provideService(credential, HttpServerRequest.HttpServerRequest, request);
119
- const token = Redacted.value(bearer);
120
- const verified = token === "" ? undefined : yield* Effect.promise(() => verifyRuntime(token));
121
- switch (verified?.status) {
122
- case "valid":
123
- break;
124
- case "invalid":
125
- yield* logRuntimeRejection(verified.reason);
126
- return toServerResponse(unauthorized(requestId, "The bearer token is invalid, expired or revoked."));
127
- case "unavailable":
128
- return toServerResponse(unavailable(requestId));
129
- default:
130
- return toServerResponse(forbidden(requestId, "Only the app's agent calls this route."));
131
- }
132
- const actAs = request.headers[ACT_AS_HEADER]?.trim() ?? "";
133
- const conversation = request.headers[CONVERSATION_HEADER]?.trim() ?? "";
134
- const session = request.headers[SESSION_HEADER]?.trim() ?? "";
135
- const member = actAs === "" ? undefined : yield* agents.actingMember(actAs);
136
- if (member === undefined) {
137
- return toServerResponse(forbidden(requestId, "The member is inactive or has not paired with the agent."));
138
- }
139
- if (!CONVERSATION.test(conversation) || (session !== "" && !SESSION.test(session))) {
140
- return toServerResponse(forbidden(requestId, "The agent names the chat and session of every request."));
141
- }
142
- return yield* answer(request, {
143
- principal: member,
144
- conversation,
145
- ...(session === "" ? {} : { sessionId: session }),
146
- }, attachments);
110
+ const authenticated = yield* authenticateAgentCaller({
111
+ request,
112
+ agents,
113
+ verify: verifyRuntime,
114
+ });
115
+ return authenticated.ok
116
+ ? yield* answer(request, authenticated.caller, attachments)
117
+ : authenticated.response;
147
118
  });
148
119
  const idOf = Effect.map(HttpRouter.params, (params) => params.id ?? "");
149
120
  yield* router.add("POST", ATTACHMENTS_PATH, asAgent((request, caller, runtime) => answered(Effect.gen(function* () {
@@ -0,0 +1,20 @@
1
+ import { Effect } from "effect";
2
+ import { HttpServerRequest } from "effect/unstable/http";
3
+ import type { AttachmentCaller } from "../../storage/attachments.js";
4
+ import { RequestId } from "../errors.js";
5
+ import type { AgentStore } from "../runtime-contract.js";
6
+ import { type RuntimeTokenVerifier } from "../runtime-identity.js";
7
+ export declare const CONVERSATION_HEADER = "x-segmentstream-agent-conversation";
8
+ export declare const SESSION_HEADER = "x-segmentstream-agent-session";
9
+ /** Both temporary attachments and stored files require the runtime and its paired member. */
10
+ export declare function authenticateAgentCaller(input: {
11
+ readonly request: HttpServerRequest.HttpServerRequest;
12
+ readonly agents: AgentStore;
13
+ readonly verify: RuntimeTokenVerifier;
14
+ }): Effect.Effect<{
15
+ readonly ok: false;
16
+ readonly response: import("effect/unstable/http/HttpServerResponse").HttpServerResponse;
17
+ } | {
18
+ readonly ok: true;
19
+ readonly caller: AttachmentCaller;
20
+ }, never, RequestId | HttpServerRequest.ParsedSearchParams>;
@@ -0,0 +1,48 @@
1
+ import { Effect, Redacted } from "effect";
2
+ import { HttpServerRequest } from "effect/unstable/http";
3
+ import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi";
4
+ import { RequestId, forbidden, toServerResponse, unauthorized, unavailable } from "../errors.js";
5
+ import { ACT_AS_HEADER, logRuntimeRejection, } from "../runtime-identity.js";
6
+ export const CONVERSATION_HEADER = "x-segmentstream-agent-conversation";
7
+ export const SESSION_HEADER = "x-segmentstream-agent-session";
8
+ const CONVERSATION = /^[a-z][a-z0-9_-]{0,31}:[A-Za-z0-9:_-]{1,200}$/;
9
+ const SESSION = /^[A-Za-z0-9:_.-]{1,200}$/;
10
+ /** Both temporary attachments and stored files require the runtime and its paired member. */
11
+ export function authenticateAgentCaller(input) {
12
+ return Effect.gen(function* () {
13
+ const { request, agents, verify } = input;
14
+ const requestId = yield* RequestId;
15
+ const credential = HttpApiBuilder.securityDecode(HttpApiSecurity.bearer);
16
+ const bearer = yield* Effect.provideService(credential, HttpServerRequest.HttpServerRequest, request);
17
+ const token = Redacted.value(bearer);
18
+ const verified = token === "" ? undefined : yield* Effect.promise(() => verify(token));
19
+ const denied = (response) => ({ ok: false, response: toServerResponse(response) });
20
+ switch (verified?.status) {
21
+ case "valid":
22
+ break;
23
+ case "invalid":
24
+ yield* logRuntimeRejection(verified.reason);
25
+ return denied(unauthorized(requestId, "The bearer token is invalid, expired or revoked."));
26
+ case "unavailable":
27
+ return denied(unavailable(requestId));
28
+ default:
29
+ return denied(forbidden(requestId, "Only the app's agent calls this route."));
30
+ }
31
+ const actAs = request.headers[ACT_AS_HEADER]?.trim() ?? "";
32
+ const conversation = request.headers[CONVERSATION_HEADER]?.trim() ?? "";
33
+ const session = request.headers[SESSION_HEADER]?.trim() ?? "";
34
+ const member = actAs === "" ? undefined : yield* agents.actingMember(actAs);
35
+ if (member === undefined) {
36
+ return denied(forbidden(requestId, "The member is inactive or has not paired with the agent."));
37
+ }
38
+ if (!CONVERSATION.test(conversation) || (session !== "" && !SESSION.test(session))) {
39
+ return denied(forbidden(requestId, "The agent names the chat and session of every request."));
40
+ }
41
+ const caller = {
42
+ principal: member,
43
+ conversation,
44
+ ...(session === "" ? {} : { sessionId: session }),
45
+ };
46
+ return { ok: true, caller };
47
+ });
48
+ }
@@ -0,0 +1,14 @@
1
+ import { Layer } from "effect";
2
+ import { HttpRouter } from "effect/unstable/http";
3
+ import { type SessionExchange, type TokenVerifier } from "../authentication.js";
4
+ import { RequestId } from "../errors.js";
5
+ import { OntologyBackend } from "../runtime-contract.js";
6
+ import { type RuntimeTokenVerifier } from "../runtime-identity.js";
7
+ import { type ConversationTransport } from "./conversations.js";
8
+ /** Same member scope as MCP, accessible to app clients with their usual bearer/session. */
9
+ export declare function conversationRoutes(options: {
10
+ readonly transport: ConversationTransport;
11
+ readonly verifyToken: TokenVerifier;
12
+ readonly verifyRuntime?: RuntimeTokenVerifier | undefined;
13
+ readonly exchangeSession?: SessionExchange | undefined;
14
+ }): Layer.Layer<never, never, OntologyBackend | HttpRouter.HttpRouter | HttpRouter.Request<"Requires", RequestId>>;
@@ -0,0 +1,43 @@
1
+ import { Effect, Layer, Schema } from "effect";
2
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
3
+ import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi";
4
+ import { CurrentPrincipal, withPrincipal, } from "../authentication.js";
5
+ import { RequestId, expectedFailure, errorBodyForWire } from "../errors.js";
6
+ import { identityToken } from "../operations.js";
7
+ import { OntologyBackend } from "../runtime-contract.js";
8
+ import { runtimeCredential } from "../runtime-identity.js";
9
+ import { ConversationSearch } from "./conversations.js";
10
+ /** Same member scope as MCP, accessible to app clients with their usual bearer/session. */
11
+ export function conversationRoutes(options) {
12
+ return Layer.effectDiscard(Effect.gen(function* () {
13
+ const router = yield* HttpRouter.HttpRouter;
14
+ const backend = yield* OntologyBackend;
15
+ yield* router.add("POST", "/api/ontology/agent/conversations/search", (request) => Effect.gen(function* () {
16
+ const bearer = yield* HttpApiBuilder.securityDecode(HttpApiSecurity.bearer).pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, request));
17
+ const operation = Effect.gen(function* () {
18
+ const search = yield* Schema.decodeUnknownEffect(ConversationSearch)(yield* request.json);
19
+ const principal = yield* CurrentPrincipal;
20
+ const page = yield* options.transport({
21
+ search,
22
+ principal,
23
+ identityToken: identityToken(request),
24
+ });
25
+ return HttpServerResponse.jsonUnsafe(page, {
26
+ headers: { "cache-control": "private, no-store" },
27
+ });
28
+ }).pipe(Effect.catch((error) => Effect.gen(function* () {
29
+ const requestId = yield* RequestId;
30
+ const failure = expectedFailure(error, requestId);
31
+ return HttpServerResponse.jsonUnsafe(errorBodyForWire(failure?.body ?? {
32
+ error: "invalid_request",
33
+ detail: "The conversation request is invalid.",
34
+ requestId,
35
+ }), { status: failure?.status ?? 400 });
36
+ })));
37
+ return yield* withPrincipal(bearer, options.verifyToken, backend, operation, {
38
+ runtime: runtimeCredential(options.verifyRuntime, request),
39
+ session: { request, exchange: options.exchangeSession },
40
+ });
41
+ }));
42
+ }));
43
+ }
@@ -0,0 +1,35 @@
1
+ import { Effect, Schema } from "effect";
2
+ import type { Principal } from "../../runtime.js";
3
+ import { PermissionDenied } from "../../errors.js";
4
+ import type { OntologyEnvironment } from "../config.js";
5
+ export declare const ConversationSearch: Schema.Struct<{
6
+ readonly q: Schema.optionalKey<Schema.String>;
7
+ readonly provider: Schema.optionalKey<Schema.String>;
8
+ readonly chatId: Schema.optionalKey<Schema.String>;
9
+ readonly threadId: Schema.optionalKey<Schema.String>;
10
+ readonly senderId: Schema.optionalKey<Schema.String>;
11
+ readonly from: Schema.optionalKey<Schema.String>;
12
+ readonly to: Schema.optionalKey<Schema.String>;
13
+ readonly before: Schema.optionalKey<Schema.String>;
14
+ readonly limit: Schema.optionalKey<Schema.Int>;
15
+ }>;
16
+ export type ConversationSearch = typeof ConversationSearch.Type;
17
+ export declare const ConversationPage: Schema.Struct<{
18
+ readonly events: Schema.$Array<Schema.$Record<Schema.String, Schema.Unknown>>;
19
+ readonly nextCursor: Schema.NullOr<Schema.String>;
20
+ }>;
21
+ export type ConversationPage = typeof ConversationPage.Type;
22
+ declare const ConversationArchiveUnavailable_base: Schema.Class<ConversationArchiveUnavailable, Schema.TaggedStruct<"ConversationArchiveUnavailable", {
23
+ readonly diagnostic: Schema.String;
24
+ readonly cause: Schema.optionalKey<Schema.Unknown>;
25
+ }>, import("effect/Cause").YieldableError>;
26
+ export declare class ConversationArchiveUnavailable extends ConversationArchiveUnavailable_base {
27
+ }
28
+ export type ConversationTransport = (input: {
29
+ readonly search: ConversationSearch;
30
+ readonly principal: Principal;
31
+ readonly identityToken: string | undefined;
32
+ }) => Effect.Effect<ConversationPage, ConversationArchiveUnavailable | PermissionDenied>;
33
+ /** The authenticated ontology service forwards its resolved member, never a caller-supplied ID. */
34
+ export declare function platformConversations(environment: OntologyEnvironment, fetchImpl?: typeof fetch): ConversationTransport;
35
+ export {};
@@ -0,0 +1,68 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { PermissionDenied } from "../../errors.js";
3
+ import { isAllowed } from "../../permissions.js";
4
+ import { describeError } from "../aggregate-platform.js";
5
+ const Filter = Schema.String.check(Schema.isMaxLength(1000));
6
+ export const ConversationSearch = Schema.Struct({
7
+ q: Schema.optionalKey(Filter),
8
+ provider: Schema.optionalKey(Filter),
9
+ chatId: Schema.optionalKey(Filter),
10
+ threadId: Schema.optionalKey(Filter),
11
+ senderId: Schema.optionalKey(Filter),
12
+ from: Schema.optionalKey(Filter),
13
+ to: Schema.optionalKey(Filter),
14
+ before: Schema.optionalKey(Filter),
15
+ limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 200 }))),
16
+ });
17
+ export const ConversationPage = Schema.Struct({
18
+ events: Schema.Array(Schema.Record(Schema.String, Schema.Unknown)),
19
+ nextCursor: Schema.NullOr(Schema.String),
20
+ });
21
+ export class ConversationArchiveUnavailable extends Schema.TaggedError()("ConversationArchiveUnavailable", { diagnostic: Schema.String, cause: Schema.optionalKey(Schema.Unknown) }) {
22
+ }
23
+ /** The authenticated ontology service forwards its resolved member, never a caller-supplied ID. */
24
+ export function platformConversations(environment, fetchImpl = fetch) {
25
+ return ({ search, principal, identityToken }) => {
26
+ if (principal.kind !== "member") {
27
+ return Effect.fail(new PermissionDenied({
28
+ message: "Only members may read conversations.",
29
+ permission: "conversation:participant",
30
+ }));
31
+ }
32
+ if (!identityToken) {
33
+ return Effect.fail(new ConversationArchiveUnavailable({ diagnostic: "runtime_identity_missing" }));
34
+ }
35
+ return Effect.tryPromise({
36
+ try: async (signal) => {
37
+ const base = `${environment.segmentstreamUrl}/api/internal/v1/workspaces/`;
38
+ const response = await fetchImpl(`${base}${encodeURIComponent(environment.workspaceId)}/agent/conversations/search`, {
39
+ method: "POST",
40
+ redirect: "error",
41
+ headers: {
42
+ "content-type": "application/json",
43
+ "x-segmentstream-organization-id": environment.organizationId,
44
+ "x-segmentstream-runtime-identity": identityToken,
45
+ },
46
+ body: JSON.stringify({
47
+ search,
48
+ reader: {
49
+ participantId: principal.id,
50
+ groups: isAllowed("agent_chat:read", principal),
51
+ admin: false,
52
+ },
53
+ }),
54
+ signal: AbortSignal.any([signal, AbortSignal.timeout(15_000)]),
55
+ });
56
+ if (!response.ok) {
57
+ throw new Error(`Conversation archive returned HTTP ${response.status}; ` +
58
+ `request ${response.headers.get("x-request-id") ?? "unknown"}.`);
59
+ }
60
+ return Schema.decodeUnknownSync(ConversationPage)(await response.json());
61
+ },
62
+ catch: (cause) => new ConversationArchiveUnavailable({ diagnostic: "archive_request_failed", cause }),
63
+ }).pipe(Effect.tapError((error) => Effect.logWarning("ontology.conversations.read_failed").pipe(Effect.annotateLogs({
64
+ diagnostic: error.diagnostic,
65
+ error: describeError(error.cause, (text) => text.split(identityToken).join("[redacted]")),
66
+ }))));
67
+ };
68
+ }
@@ -0,0 +1,12 @@
1
+ import { Layer } from "effect";
2
+ import { HttpRouter } from "effect/unstable/http";
3
+ import { RequestId } from "../errors.js";
4
+ import type { ApiPlan } from "../plan.js";
5
+ import { OntologyBackend } from "../runtime-contract.js";
6
+ import type { RuntimeTokenVerifier } from "../runtime-identity.js";
7
+ export declare const FILES_PATH = "/api/ontology/agent/files";
8
+ /** A short-lived download grant, issued only after the runtime's member may read the file. */
9
+ export declare function filesLayer(options: {
10
+ readonly plan: ApiPlan;
11
+ readonly verifyRuntime: RuntimeTokenVerifier | undefined;
12
+ }): Layer.Layer<never, never, OntologyBackend | HttpRouter.HttpRouter | HttpRouter.Request<"Requires", RequestId>>;
@@ -0,0 +1,69 @@
1
+ import { Cause, Effect, Layer } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
3
+ import { resolveFileRecord } from "../../runtime/files.js";
4
+ import { isUuid } from "../../query.js";
5
+ import { RequestId, failureResponse, notFound, toServerResponse } from "../errors.js";
6
+ import { OntologyBackend } from "../runtime-contract.js";
7
+ import { authenticateAgentCaller } from "./authentication.js";
8
+ import { AGENT_PATH } from "./server.js";
9
+ export const FILES_PATH = `${AGENT_PATH}/files`;
10
+ /** A short-lived download grant, issued only after the runtime's member may read the file. */
11
+ export function filesLayer(options) {
12
+ return Layer.effectDiscard(Effect.gen(function* () {
13
+ const router = yield* HttpRouter.HttpRouter;
14
+ const backend = yield* OntologyBackend;
15
+ const { blobs, agents } = backend;
16
+ const verify = options.verifyRuntime;
17
+ if (blobs === undefined || agents === undefined || verify === undefined) {
18
+ return;
19
+ }
20
+ const read = (request) => Effect.gen(function* () {
21
+ const requestId = yield* RequestId;
22
+ const authenticated = yield* authenticateAgentCaller({ request, agents, verify });
23
+ if (!authenticated.ok) {
24
+ return authenticated.response;
25
+ }
26
+ const params = yield* HttpRouter.params;
27
+ const id = params.id ?? "";
28
+ if (!isUuid(id)) {
29
+ return toServerResponse(notFound(requestId, "File record not found."));
30
+ }
31
+ const resolved = yield* resolveFileRecord({
32
+ manifest: options.plan.ontology.manifest,
33
+ reads: backend,
34
+ principal: authenticated.caller.principal,
35
+ reference: {
36
+ entity: params.entity ?? "",
37
+ id,
38
+ ...(params.property === undefined ? {} : { property: params.property }),
39
+ },
40
+ });
41
+ // Pin the revision read above. download rechecks live permissions in its transaction.
42
+ const download = yield* blobs.download(resolved.entity, resolved.id, resolved.property, { revisionId: resolved.file.id }, authenticated.caller.principal);
43
+ return HttpServerResponse.jsonUnsafe({
44
+ file: {
45
+ id,
46
+ entity: resolved.entity,
47
+ property: resolved.property,
48
+ filename: resolved.file.filename,
49
+ content_type: resolved.file.contentType,
50
+ size: resolved.file.size,
51
+ },
52
+ download: {
53
+ url: download.url,
54
+ method: download.method,
55
+ headers: download.headers,
56
+ expires_at: download.expiresAt,
57
+ },
58
+ }, { headers: { "cache-control": "no-store" } });
59
+ }).pipe(Effect.catchTag("UnsupportedFile", (error) => Effect.map(RequestId, (requestId) => HttpServerResponse.jsonUnsafe({
60
+ error: "unsupported",
61
+ detail: error.message,
62
+ request_id: requestId,
63
+ }, { status: 422 }))), Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause)
64
+ ? Effect.failCause(cause)
65
+ : Effect.flatMap(RequestId, (requestId) => failureResponse(cause, requestId))));
66
+ yield* router.add("GET", `${FILES_PATH}/:entity/:id`, read);
67
+ yield* router.add("GET", `${FILES_PATH}/:entity/:id/:property`, read);
68
+ }));
69
+ }
@@ -170,7 +170,13 @@ function seeChat(store) {
170
170
  if (Result.isFailure(decoded)) {
171
171
  return invalidBody(requestId, schemaIssues(decoded.failure));
172
172
  }
173
- return json(yield* store.seeChat(decoded.success));
173
+ const chat = yield* store.seeChat(decoded.success);
174
+ yield* Effect.logInfo("ontology.agent.chat_reported").pipe(Effect.annotateLogs({
175
+ requestId,
176
+ provider: decoded.success.provider,
177
+ chatId: decoded.success.externalId,
178
+ }));
179
+ return json(chat);
174
180
  });
175
181
  }
176
182
  /** `GET /api/ontology/agent/identities` (member): the member's own live identities. */
@@ -385,6 +385,17 @@ export function expectedFailure(error, requestId) {
385
385
  },
386
386
  };
387
387
  }
388
+ if (Predicate.isTagged(error, "ConversationArchiveUnavailable")) {
389
+ return {
390
+ status: 502,
391
+ body: {
392
+ error: "conversation_archive_unavailable",
393
+ detail: "The conversation archive is unavailable. Try again shortly.",
394
+ requestId,
395
+ retryable: true,
396
+ },
397
+ };
398
+ }
388
399
  if (Predicate.isTagged(error, "AggregateUpstreamUnavailable")) {
389
400
  const { reason } = error;
390
401
  return {
@@ -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 ConversationTransport } from "../agent/conversations.js";
7
8
  import type { AgentChannelFactory } from "../agent/channel.js";
8
9
  import type { ConnectionTransportFactory } from "../connections.js";
9
10
  import { RequestId } from "../errors.js";
@@ -26,6 +27,7 @@ export interface McpServerOptions {
26
27
  readonly aggregates: Aggregates;
27
28
  /** The workspace's agent, for `agent_chat.send_message`; `undefined` without one. */
28
29
  readonly agentChannel?: AgentChannelFactory | undefined;
30
+ readonly conversations?: ConversationTransport | undefined;
29
31
  }
30
32
  /**
31
33
  * The ontology's MCP server at `POST /mcp`: Streamable HTTP without sessions, so any instance can
@@ -6,6 +6,7 @@ import { isAllowed, permissionOf, READ_VERB } from "../../permissions.js";
6
6
  import { SCOPE_MISMATCH } from "../aggregate-platform.js";
7
7
  import { CurrentPrincipal, withPrincipal } from "../authentication.js";
8
8
  import { catalogFor } from "../catalog.js";
9
+ import { ConversationSearch } from "../agent/conversations.js";
9
10
  import { describeCatalog, measureDescription, measuresFor, RECORDS_OR_AGGREGATES, } from "../../render.js";
10
11
  import { RequestId, errorBodyForWire, expectedFailure, internalError, schemaIssues, } from "../errors.js";
11
12
  import { publicOrigin } from "../oauth/protocol.js";
@@ -89,7 +90,11 @@ function encoded(schema, value) {
89
90
  return Schema.encodeUnknownEffect(schema)(value).pipe(Effect.orDie);
90
91
  }
91
92
  const GetArguments = Schema.Struct({ id: IdParam });
92
- const HistoryArguments = Schema.Struct({ id: IdParam, ...HistoryParams });
93
+ const HistoryArguments = Schema.Struct({
94
+ id: IdParam,
95
+ ...HistoryParams,
96
+ conversation_before: Schema.optionalKey(Schema.String),
97
+ });
93
98
  function structured(value) {
94
99
  return { text: JSON.stringify(value), structured: value };
95
100
  }
@@ -163,7 +168,7 @@ function toolFailure(cause, requestId) {
163
168
  isError: true,
164
169
  }));
165
170
  }
166
- function runTool({ built, operations, aggregate }, tool, args, request) {
171
+ function runTool({ built, operations, aggregate, conversations }, tool, args, request) {
167
172
  return Effect.gen(function* () {
168
173
  const { target } = tool;
169
174
  switch (target.kind) {
@@ -191,9 +196,30 @@ function runTool({ built, operations, aggregate }, tool, args, request) {
191
196
  const { id } = yield* decodeArguments(GetArguments, args);
192
197
  return structured(yield* encoded(schemas.record, yield* operations.get(target.planned, id)));
193
198
  }
199
+ case "conversation-search": {
200
+ const search = yield* decodeArguments(ConversationSearch, args);
201
+ const principal = yield* CurrentPrincipal;
202
+ return structured(yield* conversations({ search, principal, identityToken: identityToken(request) }));
203
+ }
194
204
  case "history": {
195
- const { id, ...params } = yield* decodeArguments(HistoryArguments, args);
196
- return structured(yield* encoded(HistoryPage, yield* operations.history(target.planned, id, params)));
205
+ const { id, conversation_before, ...params } = yield* decodeArguments(HistoryArguments, args);
206
+ const audit = yield* encoded(HistoryPage, yield* operations.history(target.planned, id, params));
207
+ if (target.planned.name !== "agent_chat" || conversations === undefined) {
208
+ return structured(audit);
209
+ }
210
+ const chat = yield* operations.get(target.planned, id);
211
+ const principal = yield* CurrentPrincipal;
212
+ const conversation = yield* conversations({
213
+ principal,
214
+ identityToken: identityToken(request),
215
+ search: {
216
+ provider: String(chat.provider),
217
+ chatId: String(chat.external_id),
218
+ ...(params.limit === undefined ? {} : { limit: params.limit }),
219
+ ...(conversation_before === undefined ? {} : { before: conversation_before }),
220
+ },
221
+ });
222
+ return structured({ ...audit, conversation });
197
223
  }
198
224
  case "action": {
199
225
  const { [ACTION_OPTIONS_ARGUMENT]: rawOptions, ...input } = args;
@@ -433,7 +459,7 @@ function handlePost(server, verifyToken, verifyRuntime, backend) {
433
459
  * authentication. `GET` and `DELETE`, which open a server stream and end a session, answer 405.
434
460
  */
435
461
  export function mcpLayer(options) {
436
- const tools = mcpTools(options.built);
462
+ const tools = mcpTools(options.built, options.conversations !== undefined);
437
463
  return Layer.effectDiscard(Effect.gen(function* () {
438
464
  const router = yield* HttpRouter.HttpRouter;
439
465
  const backend = yield* OntologyBackend;
@@ -442,6 +468,7 @@ export function mcpLayer(options) {
442
468
  tools,
443
469
  operations: makeOperations(backend, options.built.plan, options.hooks, options.connections, options.replicate, options.agentChannel),
444
470
  aggregate: options.aggregates.model,
471
+ conversations: options.conversations,
445
472
  };
446
473
  yield* router.add("POST", MCP_PATH, handlePost(server, options.verifyToken, options.verifyRuntime, backend));
447
474
  yield* router.add("GET", MCP_PATH, METHOD_NOT_ALLOWED);
@@ -26,6 +26,8 @@ export type McpToolTarget = {
26
26
  readonly kind: "me";
27
27
  } | {
28
28
  readonly kind: "aggregate";
29
+ } | {
30
+ readonly kind: "conversation-search";
29
31
  };
30
32
  /** A tool of the ontology's MCP server, derived from the same plan as the HTTP routes. */
31
33
  export interface McpTool {
@@ -58,7 +60,7 @@ export declare const ACTION_OPTIONS_ARGUMENT = "_options";
58
60
  * as they have no record routes. Throws {@link InvalidOntology} when two tools share a name, which
59
61
  * `check` already refuses.
60
62
  */
61
- export declare function mcpTools(built: BuiltApi): ReadonlyArray<McpTool>;
63
+ export declare function mcpTools(built: BuiltApi, conversations?: boolean): ReadonlyArray<McpTool>;
62
64
  /**
63
65
  * The tools a caller may see: those whose every permission the caller's roles grant, each with the
64
66
  * caller's input schema.
@@ -409,7 +409,7 @@ function aggregateTool(manifest) {
409
409
  * as they have no record routes. Throws {@link InvalidOntology} when two tools share a name, which
410
410
  * `check` already refuses.
411
411
  */
412
- export function mcpTools(built) {
412
+ export function mcpTools(built, conversations = false) {
413
413
  const { manifest } = built.plan.ontology;
414
414
  const tools = [
415
415
  ...GLOBAL_TOOLS,
@@ -423,6 +423,50 @@ export function mcpTools(built) {
423
423
  tools.push(...readTools(planned, schemas, built));
424
424
  tools.push(...planned.actions.map((action) => actionTool(planned, action)));
425
425
  }
426
+ if (conversations) {
427
+ const index = tools.findIndex((tool) => tool.name === "agent_chat_history");
428
+ if (index >= 0) {
429
+ const tool = tools[index];
430
+ const rest = { ...tool };
431
+ delete rest.outputSchema;
432
+ tools[index] = {
433
+ ...rest,
434
+ description: tool.description +
435
+ " Also returns conversation.events: message texts, reply/silent decisions and reasons, " +
436
+ "tool calls/results and attachment references. Conversation pagination uses conversation_before " +
437
+ "separately from the audit after cursor.",
438
+ inputSchema: {
439
+ ...tool.inputSchema,
440
+ properties: {
441
+ ...tool.inputSchema.properties,
442
+ conversation_before: {
443
+ type: "string",
444
+ description: "conversation.nextCursor from the previous page.",
445
+ },
446
+ },
447
+ },
448
+ };
449
+ }
450
+ tools.push({
451
+ name: "agent_chat_search",
452
+ title: "Search stored conversations",
453
+ description: "Search retained conversation messages, decisions and tool calls by text, provider chat ID, " +
454
+ "sender ID, topic and date range. Returns only groups you may read (agent_chat:read) and DMs " +
455
+ "in which you participated. Dates are ISO 8601; before is the nextCursor of the previous page. " +
456
+ "Workspace administrators can read all DMs through the platform API.",
457
+ inputSchema: {
458
+ type: "object",
459
+ additionalProperties: false,
460
+ properties: {
461
+ ...Object.fromEntries(["q", "provider", "chatId", "threadId", "senderId", "from", "to", "before"].map((key) => [key, { type: "string", maxLength: 1000 }])),
462
+ limit: { type: "integer", minimum: 1, maximum: 200 },
463
+ },
464
+ },
465
+ annotations: READ,
466
+ permissions: [],
467
+ target: { kind: "conversation-search" },
468
+ });
469
+ }
426
470
  const seen = new Set();
427
471
  const duplicates = tools.filter(({ name }) => seen.size === seen.add(name).size);
428
472
  if (duplicates.length > 0) {
@@ -5,6 +5,9 @@ import { storageMaintenanceAuthenticationLayer } from "../storage/maintenance-au
5
5
  import { nameToWire } from "../authoring-names.js";
6
6
  import { sqlServiceRuntime } from "../runtime/service-binding.js";
7
7
  import { attachmentsLayer } from "./agent/attachments.js";
8
+ import { filesLayer } from "./agent/files.js";
9
+ import { platformConversations } from "./agent/conversations.js";
10
+ import { conversationRoutes } from "./agent/conversation-routes.js";
8
11
  import { deployedAgentChannel } from "./agent/channel.js";
9
12
  import { agentLayer } from "./agent/server.js";
10
13
  import { makeAggregates } from "./aggregate.js";
@@ -95,6 +98,7 @@ export function createOntologyService(ontology, config) {
95
98
  const agentChannel = config.runtimeIdentity === undefined || config.agentUrl === undefined
96
99
  ? undefined
97
100
  : deployedAgentChannel(config.agentUrl);
101
+ const conversations = config.runtimeIdentity === undefined ? undefined : platformConversations(config);
98
102
  const aggregates = makeAggregates({
99
103
  manifest: ontology.manifest,
100
104
  transport: platformAggregates({ ...config, manifestHash: ontology.manifest.hash }),
@@ -115,6 +119,7 @@ export function createOntologyService(ontology, config) {
115
119
  replicate,
116
120
  aggregates,
117
121
  agentChannel,
122
+ conversations,
118
123
  }), oauthLayer({ verifyToken }));
119
124
  const agent = agentLayer({ verifyToken, verifyRuntime, exchangeSession });
120
125
  const attachments = attachmentsLayer({
@@ -126,7 +131,14 @@ export function createOntologyService(ontology, config) {
126
131
  verifyToken,
127
132
  verifyRuntime,
128
133
  });
129
- const application = Layer.mergeAll(routes, mcp, agent, attachments, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
134
+ const application = Layer.mergeAll(routes, mcp, agent, attachments, filesLayer({ plan: built.plan, verifyRuntime }), conversations === undefined
135
+ ? Layer.empty
136
+ : conversationRoutes({
137
+ transport: conversations,
138
+ verifyToken,
139
+ verifyRuntime,
140
+ exchangeSession,
141
+ }), DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
130
142
  const web = HttpRouter.toWebHandler(application, { disableLogger: true });
131
143
  const hash = ontology.manifest.hash;
132
144
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/ontology",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",