@segmentstream-cloud/ontology 0.15.0 → 0.17.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/dist/measure.js CHANGED
@@ -1,23 +1,36 @@
1
+ /** The aggregates a measure expression may contain, `count` included. */
2
+ export const MEASURE_AGGREGATES = ["count", "sum", "countDistinct", "avg", "min", "max"];
1
3
  function binary(op) {
2
4
  return (left, right, options) => ({ op, args: [left, right], ...options });
3
5
  }
4
- /** Arithmetic combines aggregates; references name another measure of the same entity. */
5
- export const m = {
6
- count: (options) => ({ op: "count", ...options }),
7
- sum: (property, options) => ({
8
- op: "sum",
6
+ /** An aggregate of one property of the entity. */
7
+ function aggregate(op) {
8
+ return (property, options) => ({
9
+ op,
9
10
  args: [{ op: "column", name: property }],
10
11
  ...options,
11
- }),
12
+ });
13
+ }
14
+ /**
15
+ * Arithmetic combines aggregates; references name another measure of the same entity. Every
16
+ * aggregate takes `where`, conditions on properties of the entity that the rows it reads must meet,
17
+ * and `balanceOver`, the date its rows are snapshots over.
18
+ */
19
+ export const m = {
20
+ count: (options) => ({ op: "count", ...options }),
21
+ /** The sum of an integer or number property. */
22
+ sum: aggregate("sum"),
12
23
  /**
13
24
  * The number of distinct non-null values of a text, email, url, select, integer, date or timestamp
14
25
  * property. Distinct counts do not add up across groups.
15
26
  */
16
- countDistinct: (property, options) => ({
17
- op: "countDistinct",
18
- args: [{ op: "column", name: property }],
19
- ...options,
20
- }),
27
+ countDistinct: aggregate("countDistinct"),
28
+ /** The average of an integer or number property, ignoring nulls. Averages do not add up. */
29
+ avg: aggregate("avg"),
30
+ /** The least value of an integer or number property. Minimums do not add up. */
31
+ min: aggregate("min"),
32
+ /** The greatest value of an integer or number property. Maximums do not add up. */
33
+ max: aggregate("max"),
21
34
  ref: (name, options) => ({ op: "measure", name, ...options }),
22
35
  literal: (value, options) => ({
23
36
  op: "literal",
@@ -29,12 +42,52 @@ export const m = {
29
42
  multiply: binary("multiply"),
30
43
  divide: binary("divide"),
31
44
  };
45
+ /** The conditions an expression declares: an aggregate's `where` option, or a `filtered` node's. */
46
+ export function conditionsOf(value) {
47
+ return "where" in value ? value.where : undefined;
48
+ }
49
+ /** The balance property an expression node declares: an aggregate's `balanceOver` option. */
50
+ export function balanceOf(value) {
51
+ return "balanceOver" in value ? value.balanceOver : undefined;
52
+ }
32
53
  /**
33
- * Strip authoring options and resolve logical properties to physical columns for the manifest: a
34
- * warehouse-backed property's `column`, or a stored property's replication view column, which is its
35
- * wire name.
54
+ * The balance properties a measure's own aggregates declare, top-level or inside arithmetic and
55
+ * `filtered` expressions, without duplicates. References are not followed: the platform inherits a
56
+ * referenced measure's balance itself.
36
57
  */
37
- export function measureExpression(value, columns) {
58
+ export function balancesOf(value) {
59
+ const balances = new Set();
60
+ const visit = (expression, depth) => {
61
+ const balance = balanceOf(expression);
62
+ if (typeof balance === "string") {
63
+ balances.add(balance);
64
+ }
65
+ if ("args" in expression && Array.isArray(expression.args) && depth < 30) {
66
+ for (const arg of expression.args) {
67
+ visit(arg, depth + 1);
68
+ }
69
+ }
70
+ };
71
+ visit(value, 0);
72
+ return [...balances];
73
+ }
74
+ /** The property a condition names: `property` as authored, `column` in a `filtered` expression. */
75
+ export function conditionProperty(condition) {
76
+ if ("property" in condition) {
77
+ return condition.property;
78
+ }
79
+ return "column" in condition ? condition.column : undefined;
80
+ }
81
+ function columnCondition(condition, columns) {
82
+ const property = "property" in condition ? condition.property : condition.column;
83
+ return {
84
+ column: columns.get(property) ?? property,
85
+ operator: condition.operator,
86
+ ...(condition.value === undefined ? {} : { value: condition.value }),
87
+ ...(condition.values === undefined ? {} : { values: [...condition.values] }),
88
+ };
89
+ }
90
+ function bareExpression(value, columns) {
38
91
  switch (value.op) {
39
92
  case "count":
40
93
  return { op: "count" };
@@ -44,7 +97,31 @@ export function measureExpression(value, columns) {
44
97
  return { op: "measure", name: value.name };
45
98
  case "literal":
46
99
  return { op: "literal", value: value.value };
100
+ case "filtered":
101
+ return {
102
+ op: "filtered",
103
+ args: value.args.map((arg) => measureExpression(arg, columns)),
104
+ where: value.where.map((condition) => columnCondition(condition, columns)),
105
+ };
47
106
  default:
48
107
  return { op: value.op, args: value.args.map((arg) => measureExpression(arg, columns)) };
49
108
  }
50
109
  }
110
+ /**
111
+ * Strip authoring options and resolve logical properties to physical columns for the manifest: a
112
+ * warehouse-backed property's `column`, or a stored property's replication view column, which is its
113
+ * wire name. An aggregate's `where` becomes the `filtered` expression that wraps it, its conditions
114
+ * naming columns too.
115
+ */
116
+ export function measureExpression(value, columns) {
117
+ const expression = bareExpression(value, columns);
118
+ const where = conditionsOf(value);
119
+ if (where === undefined || value.op === "filtered") {
120
+ return expression;
121
+ }
122
+ return {
123
+ op: "filtered",
124
+ args: [expression],
125
+ where: where.map((condition) => columnCondition(condition, columns)),
126
+ };
127
+ }
@@ -6,6 +6,7 @@ import { isNoopMigration } from "./model.js";
6
6
  import { planMigration } from "./plan.js";
7
7
  import { REPLICATION_DDL, REPLICATION_SCHEMA_VERSION, replicationEntityDdl, } from "../replication/schema.js";
8
8
  import { createReplicationBaseline } from "../replication/baseline.js";
9
+ import { ATTACHMENTS_TABLE } from "../storage/tables.js";
9
10
  import { resolveReplicationSelection } from "../replication/model.js";
10
11
  import { AGENT_TABLES, API_KEYS_TABLE, CATALOG_TABLE, OAUTH_TABLES, USER_ROLES_TABLE, quoteIdentifier, quoteLiteral, } from "./storage.js";
11
12
  const CATALOG = quoteIdentifier(CATALOG_TABLE);
@@ -172,6 +173,7 @@ export const RUNTIME_TABLES = [
172
173
  API_KEYS_TABLE,
173
174
  ...OAUTH_TABLES,
174
175
  ...AGENT_TABLES,
176
+ ATTACHMENTS_TABLE,
175
177
  ];
176
178
  /**
177
179
  * The runtime tables the database has, for `planMigration`'s `existingTables`: a database that an
@@ -44,6 +44,17 @@ export declare const MigrationPlan: Schema.Struct<{
44
44
  readonly currentHash: Schema.optionalKey<Schema.String>;
45
45
  readonly target: Schema.Union<readonly [Schema.encodeKeys<Schema.Struct<{
46
46
  readonly entities: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
47
+ readonly measures: Schema.optionalKey<Schema.$Array<Schema.encodeKeys<Schema.Struct<{
48
+ readonly name: Schema.String;
49
+ readonly label: Schema.String;
50
+ readonly description: Schema.String;
51
+ readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
52
+ readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
53
+ readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
54
+ readonly balanceOver: Schema.optionalKey<Schema.String>;
55
+ }>, {
56
+ readonly balanceOver: "balance_over";
57
+ }>>>;
47
58
  readonly properties: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
48
59
  readonly column: Schema.optionalKey<Schema.String>;
49
60
  readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
@@ -96,14 +107,6 @@ export declare const MigrationPlan: Schema.Struct<{
96
107
  readonly key: Schema.$Array<Schema.String>;
97
108
  readonly currency: Schema.optionalKey<Schema.String>;
98
109
  }>>;
99
- readonly measures: Schema.optionalKey<Schema.$Array<Schema.Struct<{
100
- readonly name: Schema.String;
101
- readonly label: Schema.String;
102
- readonly description: Schema.String;
103
- readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
104
- readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
105
- readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
106
- }>>>;
107
110
  readonly id: Schema.String;
108
111
  readonly name: Schema.String;
109
112
  readonly label: Schema.String;
@@ -175,6 +178,7 @@ export declare const MigrationPlan: Schema.Struct<{
175
178
  readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
176
179
  readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
177
180
  readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
181
+ readonly balanceOver: Schema.optionalKey<Schema.String>;
178
182
  }>>>;
179
183
  readonly id: Schema.String;
180
184
  readonly name: Schema.String;
@@ -1,5 +1,5 @@
1
1
  import { isValueMatched, } from "../manifest.js";
2
- import { BLOB_TABLES_DDL } from "../storage/tables.js";
2
+ import { ATTACHMENTS_TABLE, ATTACHMENT_TABLES_DDL, BLOB_TABLES_DDL } from "../storage/tables.js";
3
3
  import { SNAKE_CASE_NAME, isSystemProperty, toSnakeCase } from "../names.js";
4
4
  import { AGENT_TABLES, AGENT_TABLES_DDL, CATALOG_TABLE_DDL, MAX_IDENTIFIER_LENGTH, OAUTH_TABLES, OAUTH_TABLES_DDL, USER_TABLE, addConstraintStatement, columnDefinition, constraintDefinition, createEnumStatement, createIndexStatement, createTableStatement, defaultExpression, entityStorage, enumTypeName, fieldColumnName, joinTable, propertyCheck, blobForeignKey, propertyColumn, propertyEnum, quoteIdentifier, quoteLiteral, relationshipColumn, RUNTIME_TABLES_DDL, relationshipForeignKey, SYSTEM_COLUMNS, tableName, } from "./storage.js";
5
5
  const GLOBAL_KINDS = new Set(["table", "type", "index"]);
@@ -94,13 +94,23 @@ export function planMigration(input) {
94
94
  }));
95
95
  }
96
96
  }
97
- if (storedTargets.some((entity) => entity.properties.some((property) => property.type === "blob")) &&
97
+ const storesFiles = storedTargets.some((entity) => entity.properties.some((property) => property.type === "blob"));
98
+ if (storesFiles &&
98
99
  !current?.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))) {
99
100
  draft.runtimeTables.push(step({
100
101
  kind: "createCatalog",
101
102
  classification: "additive",
102
- description: "Create the ontology's file revision and upload tables.",
103
- sql: BLOB_TABLES_DDL,
103
+ description: "Create the ontology's file revision, upload and agent attachment tables.",
104
+ sql: [...BLOB_TABLES_DDL, ...ATTACHMENT_TABLES_DDL],
105
+ }));
106
+ }
107
+ else if (storesFiles && input.existingTables?.has(ATTACHMENTS_TABLE) === false) {
108
+ // File storage an earlier SDK created lacks the agent's attachments, added since.
109
+ draft.runtimeTables.push(step({
110
+ kind: "createCatalog",
111
+ classification: "additive",
112
+ description: "Create the agent's temporary attachment table beside the file tables.",
113
+ sql: ATTACHMENT_TABLES_DDL,
104
114
  }));
105
115
  }
106
116
  planDrops({ pairs, removedEntities, addedEntities, allowDrops, draft });
package/dist/render.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ActionManifest, EntityManifest, OntologyManifest } from "./manifest.js";
1
+ import type { ActionManifest, EntityManifest, MeasureManifest, OntologyManifest } from "./manifest.js";
2
2
  export interface RenderOptions {
3
3
  /**
4
4
  * The permissions of the reader. Only the actions these permissions may run are listed, and only
@@ -9,6 +9,11 @@ export interface RenderOptions {
9
9
  }
10
10
  /** What an action of the manifest requires; version 1 manifests leave it implicit. */
11
11
  export declare function actionPermission(entity: string, action: ActionManifest): string;
12
+ /**
13
+ * A measure's description followed by its conditions and balance note, as `describe`, the MCP
14
+ * measure list and the console show it: `Revenue of won orders. Where status = "won".`
15
+ */
16
+ export declare function measureDescription(entity: EntityManifest, measure: MeasureManifest): string;
12
17
  /**
13
18
  * Renders the catalog as prose for agents and people: each entity with its properties, both
14
19
  * directions of its relationships and its actions, then the roles with their permissions. Without a
@@ -22,7 +27,6 @@ export declare function renderCatalog(manifest: OntologyManifest, options?: Rend
22
27
  * or everything without them.
23
28
  */
24
29
  export declare function describeCatalog(manifest: OntologyManifest, permissions?: ReadonlyArray<string>): string;
25
- type MeasureManifest = NonNullable<EntityManifest["measures"]>[number];
26
30
  /** A measure as aggregates name it: `<entity>.<measure>`. */
27
31
  export interface NamedMeasure {
28
32
  readonly name: string;
@@ -59,4 +63,3 @@ export declare function reachesOf(manifest: OntologyManifest, from: string, perm
59
63
  * per measure entity the entities it reaches. Empty when the reader may read no measure.
60
64
  */
61
65
  export declare function renderAggregates(manifest: OntologyManifest, permissions?: ReadonlyArray<string>): string;
62
- export {};
package/dist/render.js CHANGED
@@ -154,6 +154,162 @@ function section(title, lines) {
154
154
  ? []
155
155
  : [`${INDENT}${title}:`, ...lines.map((line) => `${INDENT}${INDENT}${line}`)];
156
156
  }
157
+ /** How a condition's operator reads between its property and its value. */
158
+ const CONDITION_OPERATORS = {
159
+ equals: "=",
160
+ notEquals: "!=",
161
+ gt: ">",
162
+ gte: ">=",
163
+ lt: "<",
164
+ lte: "<=",
165
+ contains: "contains",
166
+ startsWith: "starts with",
167
+ endsWith: "ends with",
168
+ };
169
+ function literal(value) {
170
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
171
+ }
172
+ /** `status = "won"`, `status in ("won", "invoiced")`, `amount >= 100`, `name contains "x"`. */
173
+ function conditionText(condition, property) {
174
+ const { operator, value } = condition;
175
+ const values = Array.isArray(condition.values) ? condition.values : [];
176
+ const list = `(${values.map(literal).join(", ")})`;
177
+ switch (operator) {
178
+ case "in":
179
+ return `${property} in ${list}`;
180
+ case "notIn":
181
+ return `${property} not in ${list}`;
182
+ case "isNull":
183
+ return `${property} is null`;
184
+ case "isNotNull":
185
+ return `${property} is not null`;
186
+ case "between":
187
+ return `${property} between ${literal(values[0])} and ${literal(values[1])}`;
188
+ default: {
189
+ const symbol = CONDITION_OPERATORS[String(operator)] ?? String(operator);
190
+ return value === undefined
191
+ ? `${property} ${symbol}`
192
+ : `${property} ${symbol} ${literal(value)}`;
193
+ }
194
+ }
195
+ }
196
+ /** How an aggregate reads in a sentence: `count`, `sum of amount`. */
197
+ const AGGREGATE_NAMES = {
198
+ count: "count",
199
+ sum: "sum",
200
+ countDistinct: "distinct count",
201
+ avg: "average",
202
+ min: "minimum",
203
+ max: "maximum",
204
+ };
205
+ /**
206
+ * Every aggregate a measure reads: its own, top-level or inside arithmetic, and those of the
207
+ * measures it references, followed once each.
208
+ */
209
+ function readAggregates(entity, measure) {
210
+ const found = [];
211
+ const visited = new Set();
212
+ const visit = (expression, where, depth) => {
213
+ if (!isRecord(expression) || depth > 30) {
214
+ return;
215
+ }
216
+ const args = Array.isArray(expression.args) ? expression.args : [];
217
+ const name = AGGREGATE_NAMES[String(expression.op)];
218
+ if (expression.op === "filtered" && Array.isArray(expression.where)) {
219
+ const conditions = expression.where.filter(isRecord);
220
+ args.forEach((arg) => visit(arg, conditions, depth + 1));
221
+ }
222
+ else if (expression.op === "measure") {
223
+ const referenced = entity.measures?.find((other) => other.name === expression.name);
224
+ if (referenced !== undefined && !visited.has(referenced.name)) {
225
+ visited.add(referenced.name);
226
+ visit(referenced.expression, undefined, depth + 1);
227
+ }
228
+ }
229
+ else if (name !== undefined) {
230
+ const [argument] = args;
231
+ const column = isRecord(argument) && argument.op === "column" ? argument.name : undefined;
232
+ found.push({ name, column, where });
233
+ }
234
+ else {
235
+ args.forEach((arg) => visit(arg, where, depth + 1));
236
+ }
237
+ };
238
+ visited.add(measure.name);
239
+ visit(measure.expression, undefined, 0);
240
+ return found;
241
+ }
242
+ /**
243
+ * The columns a measure is a balance over: its own `balanceOver`, and that of every measure its
244
+ * expression references, since an expression is a balance when any measure it reads is.
245
+ */
246
+ function balanceColumns(entity, measure) {
247
+ const columns = new Set();
248
+ const visited = new Set();
249
+ const visit = (candidate) => {
250
+ if (visited.has(candidate.name)) {
251
+ return;
252
+ }
253
+ visited.add(candidate.name);
254
+ if (candidate.balanceOver !== undefined) {
255
+ columns.add(candidate.balanceOver);
256
+ }
257
+ for (const name of referencedMeasures(candidate.expression)) {
258
+ const referenced = entity.measures?.find((other) => other.name === name);
259
+ if (referenced !== undefined) {
260
+ visit(referenced);
261
+ }
262
+ }
263
+ };
264
+ visit(measure);
265
+ return [...columns];
266
+ }
267
+ function referencedMeasures(expression) {
268
+ if (!isRecord(expression)) {
269
+ return [];
270
+ }
271
+ if (expression.op === "measure" && typeof expression.name === "string") {
272
+ return [expression.name];
273
+ }
274
+ return Array.isArray(expression.args) ? expression.args.flatMap(referencedMeasures) : [];
275
+ }
276
+ /**
277
+ * What a measure's definition adds to its description: the conditions its aggregates read rows by,
278
+ * and the property a balance must be grouped by or filtered to one day of. `Where …` only when every
279
+ * aggregate it reads, references followed, has the same conditions; otherwise a sentence per
280
+ * restricted aggregate. Columns read as the properties that map them, the names requests use.
281
+ */
282
+ function measureNotes(entity, measure) {
283
+ const propertyOf = (column) => entity.properties.find((property) => (property.column ?? property.name) === column)?.name ??
284
+ String(column);
285
+ const aggregates = readAggregates(entity, measure).map(({ name, column, where }) => ({
286
+ name: column === undefined ? name : `${name} of ${propertyOf(column)}`,
287
+ conditions: where
288
+ ?.map((condition) => conditionText(condition, propertyOf(condition.column)))
289
+ .join(" and "),
290
+ }));
291
+ const conditions = aggregates.map((aggregate) => aggregate.conditions);
292
+ const [first] = conditions;
293
+ const balances = balanceColumns(entity, measure).map((column) => `Balance over ${propertyOf(column)}: group by it or filter it to one day.`);
294
+ // "Where" holds only when every aggregate, references followed, reads by the same condition.
295
+ if (first !== undefined && conditions.every((candidate) => candidate === first)) {
296
+ return [`Where ${first}.`, ...balances];
297
+ }
298
+ const restricted = aggregates.flatMap(({ name, conditions: text }) => text === undefined ? [] : [`Its ${name} reads rows where ${text}.`]);
299
+ return [...new Set(restricted), ...balances];
300
+ }
301
+ /**
302
+ * A measure's description followed by its conditions and balance note, as `describe`, the MCP
303
+ * measure list and the console show it: `Revenue of won orders. Where status = "won".`
304
+ */
305
+ export function measureDescription(entity, measure) {
306
+ const notes = measureNotes(entity, measure);
307
+ if (notes.length === 0) {
308
+ return measure.description;
309
+ }
310
+ const described = measure.description === "" ? [] : [sentence(measure.description)];
311
+ return [...described, ...notes].join(" ");
312
+ }
157
313
  function renderEntity(entity, manifest, entities, options) {
158
314
  const reader = options.as;
159
315
  const actions = entity.actions.filter((action) => reader === undefined ||
@@ -170,7 +326,7 @@ function renderEntity(entity, manifest, entities, options) {
170
326
  ? [`${INDENT}Source: ${entity.source.asset}; key: ${entity.source.key.join(", ")}.`]
171
327
  : []),
172
328
  ...section("Properties", entity.properties.flatMap((property) => propertyLines({ property, warehouseBacked: entity.source !== undefined }))),
173
- ...section("Measures", (entity.measures ?? []).map((measure) => `${measure.name}: ${measure.description} (${measure.unit}).`)),
329
+ ...section("Measures", (entity.measures ?? []).map((measure) => `${measure.name}: ${measureDescription(entity, measure)} (${measure.unit}).`)),
174
330
  ...section("Relationships", relationshipLines(entity, entities, manifest.entities)),
175
331
  ...section(reader === undefined ? "Actions" : "Actions you may run", actions.flatMap((action) => actionLines(action, reader === undefined
176
332
  ? requirement("Requires", actionPermission(entity.name, action), manifest.roles)
@@ -273,7 +429,7 @@ export function renderAggregates(manifest, permissions) {
273
429
  const reaches = reachesOf(manifest, owner, permissions);
274
430
  return reaches.length === 0 ? [] : [reachLine(owner, reaches)];
275
431
  });
276
- const measureLines = measures.map(({ name, measure }) => `${name} (${measure.unit}): ${sentence(measure.description)}`);
432
+ const measureLines = measures.map(({ name, entity, measure }) => `${name} (${measure.unit}): ${sentence(measureDescription(entity, measure))}`);
277
433
  return [
278
434
  `Aggregates: ${RECORDS_OR_AGGREGATES} (POST /api/ontology/aggregate, or the aggregate tool).`,
279
435
  `${INDENT}Measures, as <entity>.<measure>:`,
@@ -3,6 +3,7 @@ import { Effect, Layer, Redacted } from "effect";
3
3
  import { SqlClient } from "effect/unstable/sql";
4
4
  import { neonBlobProvider } from "../storage/neon.js";
5
5
  import { maintainBlobStorage } from "../storage/maintenance.js";
6
+ import { attachmentRuntime } from "../storage/attachments.js";
6
7
  import { blobRuntime } from "../storage/sessions.js";
7
8
  import { readCatalog } from "../migrate/apply.js";
8
9
  import { NotInvited, OntologyBackend, } from "../service/runtime-contract.js";
@@ -33,8 +34,10 @@ export function backendLayer(ontology, blobStorage) {
33
34
  ? yield* Effect.acquireRelease(Effect.sync(() => neonBlobProvider(binding)), (provider) => Effect.sync(() => provider.close?.()))
34
35
  : undefined;
35
36
  const blobs = provider ? blobRuntime(ontology, sql, provider) : undefined;
37
+ const attachments = provider ? attachmentRuntime(ontology, sql, provider) : undefined;
36
38
  return OntologyBackend.of({
37
39
  ...(blobs ? { blobs } : {}),
40
+ ...(attachments ? { attachments } : {}),
38
41
  ...(provider ? { maintainBlobs: maintainBlobStorage(sql, provider) } : {}),
39
42
  runAction: (input) => withSql(runAction(ontology, {
40
43
  entity: input.entity,
@@ -0,0 +1,37 @@
1
+ import { Layer } from "effect";
2
+ import { HttpRouter } from "effect/unstable/http";
3
+ import { type TokenVerifier } from "../authentication.js";
4
+ import type { AfterCommitHook } from "../config.js";
5
+ import type { ConnectionTransportFactory } from "../connections.js";
6
+ import { RequestId } from "../errors.js";
7
+ import type { ApiPlan } from "../plan.js";
8
+ import type { ReplicationKick } from "../replication.js";
9
+ import { OntologyBackend } from "../runtime-contract.js";
10
+ import { type RuntimeTokenVerifier } from "../runtime-identity.js";
11
+ import type { AgentChannelFactory } from "./channel.js";
12
+ /**
13
+ * The agent's temporary attachments over HTTP (`docs/agent.md`, "Attachments"). Only the
14
+ * deployment's runtime token reaches these routes, acting as the member in `X-SegmentStream-Act-As`
15
+ * while they are active and paired. The chat and the agent session come from the agent's runtime in
16
+ * `X-SegmentStream-Agent-Conversation` and `X-SegmentStream-Agent-Session`, never from the model's
17
+ * tool arguments; every operation is confined to that member, chat and session.
18
+ */
19
+ 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
+ export interface AttachmentRoutesOptions {
25
+ readonly plan: ApiPlan;
26
+ readonly hooks: ReadonlyMap<string, AfterCommitHook>;
27
+ readonly connections: ConnectionTransportFactory;
28
+ readonly replicate: ReplicationKick;
29
+ readonly agentChannel?: AgentChannelFactory | undefined;
30
+ readonly verifyToken: TokenVerifier;
31
+ readonly verifyRuntime: RuntimeTokenVerifier | undefined;
32
+ }
33
+ /**
34
+ * The attachment routes. Without file storage or the agent tables, or while runtime authentication
35
+ * is disabled, it serves nothing.
36
+ */
37
+ export declare function attachmentsLayer(options: AttachmentRoutesOptions): Layer.Layer<never, never, OntologyBackend | HttpRouter.HttpRouter | HttpRouter.Request<"Requires", RequestId>>;