@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/README.md +49 -1
- package/dist/aggregate-filters.d.ts +48 -0
- package/dist/aggregate-filters.js +252 -0
- package/dist/authoring-records.d.ts +5 -0
- package/dist/authoring-records.js +5 -1
- package/dist/authoring.js +32 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +109 -60
- package/dist/manifest.js +43 -27
- package/dist/measure.d.ts +86 -7
- package/dist/measure.js +92 -15
- package/dist/migrate/apply.js +2 -0
- package/dist/migrate/model.d.ts +12 -8
- package/dist/migrate/plan.js +14 -4
- package/dist/render.d.ts +6 -3
- package/dist/render.js +158 -2
- package/dist/runtime/service-binding.js +3 -0
- package/dist/service/agent/attachments.d.ts +37 -0
- package/dist/service/agent/attachments.js +225 -0
- package/dist/service/mcp/server.js +28 -5
- package/dist/service/mcp/tools.d.ts +6 -0
- package/dist/service/mcp/tools.js +79 -12
- package/dist/service/runtime-contract.d.ts +3 -0
- package/dist/service/schemas.d.ts +40 -22
- package/dist/service/schemas.js +46 -43
- package/dist/service/service.js +11 -1
- package/dist/storage/attachments.d.ts +117 -0
- package/dist/storage/attachments.js +536 -0
- package/dist/storage/maintenance.d.ts +4 -1
- package/dist/storage/maintenance.js +7 -2
- package/dist/storage/neon.js +14 -0
- package/dist/storage/provider.d.ts +2 -0
- package/dist/storage/tables.d.ts +8 -0
- package/dist/storage/tables.js +32 -0
- package/dist/warehouse-backed.js +199 -6
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
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";
|
|
4
|
+
import { ATTACHMENT_SOURCES, } from "../../storage/attachments.js";
|
|
5
|
+
import { schemaIssues } from "../../runtime/validation.js";
|
|
6
|
+
import { CurrentPrincipal } from "../authentication.js";
|
|
7
|
+
import { RequestId, errorBodyForWire, failureResponse, forbidden, toServerResponse, unauthorized, unavailable, } from "../errors.js";
|
|
8
|
+
import { identityToken, makeOperations } from "../operations.js";
|
|
9
|
+
import { OntologyBackend } from "../runtime-contract.js";
|
|
10
|
+
import { ACT_AS_HEADER, logRuntimeRejection, } from "../runtime-identity.js";
|
|
11
|
+
import { AGENT_PATH } from "./server.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 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
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
27
|
+
const Received = Schema.Struct({
|
|
28
|
+
source: Schema.Literals(ATTACHMENT_SOURCES),
|
|
29
|
+
source_ref: Schema.String,
|
|
30
|
+
filename: Schema.String,
|
|
31
|
+
content_type: Schema.String,
|
|
32
|
+
size: Schema.Int,
|
|
33
|
+
sha256: Schema.String,
|
|
34
|
+
});
|
|
35
|
+
const Promotion = Schema.Struct({
|
|
36
|
+
entity: Schema.String,
|
|
37
|
+
action: Schema.String,
|
|
38
|
+
input: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
|
|
39
|
+
field: Schema.optionalKey(Schema.String),
|
|
40
|
+
record_id: Schema.optionalKey(Schema.String),
|
|
41
|
+
});
|
|
42
|
+
function json(body, status = 200) {
|
|
43
|
+
return HttpServerResponse.jsonUnsafe(body, { status, headers: NO_STORE });
|
|
44
|
+
}
|
|
45
|
+
function attachmentForWire(attachment) {
|
|
46
|
+
return {
|
|
47
|
+
id: attachment.id,
|
|
48
|
+
filename: attachment.filename,
|
|
49
|
+
content_type: attachment.contentType,
|
|
50
|
+
size: attachment.size,
|
|
51
|
+
sha256: attachment.sha256,
|
|
52
|
+
state: attachment.state,
|
|
53
|
+
created_at: attachment.createdAt,
|
|
54
|
+
expires_at: attachment.expiresAt,
|
|
55
|
+
promotion: attachment.promotion,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function grantForWire(grant) {
|
|
59
|
+
return {
|
|
60
|
+
url: grant.url,
|
|
61
|
+
method: grant.method,
|
|
62
|
+
headers: grant.headers,
|
|
63
|
+
expires_at: grant.expiresAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function decodeBody(schema, request) {
|
|
67
|
+
return Effect.gen(function* () {
|
|
68
|
+
const text = yield* Effect.orDie(request.text);
|
|
69
|
+
let body;
|
|
70
|
+
try {
|
|
71
|
+
body = JSON.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
body = undefined;
|
|
75
|
+
}
|
|
76
|
+
const decoded = yield* Effect.result(Schema.decodeUnknownEffect(schema)(body, { errors: "all", onExcessProperty: "error" }));
|
|
77
|
+
return Result.isFailure(decoded)
|
|
78
|
+
? { ok: false, issues: schemaIssues(decoded.failure) }
|
|
79
|
+
: { ok: true, value: decoded.success };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function invalidBody(requestId, issues) {
|
|
83
|
+
return HttpServerResponse.jsonUnsafe(errorBodyForWire({
|
|
84
|
+
error: "invalid_request",
|
|
85
|
+
detail: "The request body is invalid.",
|
|
86
|
+
requestId,
|
|
87
|
+
issues: issues,
|
|
88
|
+
}), { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
/** Answers a failure the way every other route does: expected ones by status, the rest as 500. */
|
|
91
|
+
function answered(effect) {
|
|
92
|
+
return Effect.gen(function* () {
|
|
93
|
+
const requestId = yield* RequestId;
|
|
94
|
+
return yield* effect.pipe(Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause)
|
|
95
|
+
? Effect.failCause(cause)
|
|
96
|
+
: failureResponse(cause, requestId)));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The attachment routes. Without file storage or the agent tables, or while runtime authentication
|
|
101
|
+
* is disabled, it serves nothing.
|
|
102
|
+
*/
|
|
103
|
+
export function attachmentsLayer(options) {
|
|
104
|
+
return Layer.effectDiscard(Effect.gen(function* () {
|
|
105
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
106
|
+
const backend = yield* OntologyBackend;
|
|
107
|
+
const attachments = backend.attachments;
|
|
108
|
+
const agents = backend.agents;
|
|
109
|
+
const verifyRuntime = options.verifyRuntime;
|
|
110
|
+
if (attachments === undefined || agents === undefined || verifyRuntime === undefined) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const operations = makeOperations(backend, options.plan, options.hooks, options.connections, options.replicate, options.agentChannel);
|
|
114
|
+
const credential = HttpApiBuilder.securityDecode(HttpApiSecurity.bearer);
|
|
115
|
+
/** Authenticates the runtime, then the member it acts for, the chat and the session. */
|
|
116
|
+
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);
|
|
147
|
+
});
|
|
148
|
+
const idOf = Effect.map(HttpRouter.params, (params) => params.id ?? "");
|
|
149
|
+
yield* router.add("POST", ATTACHMENTS_PATH, asAgent((request, caller, runtime) => answered(Effect.gen(function* () {
|
|
150
|
+
const requestId = yield* RequestId;
|
|
151
|
+
const body = yield* decodeBody(Received, request);
|
|
152
|
+
if (!body.ok) {
|
|
153
|
+
return invalidBody(requestId, body.issues);
|
|
154
|
+
}
|
|
155
|
+
const received = yield* runtime.receive({
|
|
156
|
+
source: body.value.source,
|
|
157
|
+
sourceRef: body.value.source_ref,
|
|
158
|
+
filename: body.value.filename,
|
|
159
|
+
contentType: body.value.content_type,
|
|
160
|
+
size: body.value.size,
|
|
161
|
+
sha256: body.value.sha256,
|
|
162
|
+
}, caller);
|
|
163
|
+
return json({
|
|
164
|
+
attachment: attachmentForWire(received.attachment),
|
|
165
|
+
...(received.upload ? { upload: grantForWire(received.upload) } : {}),
|
|
166
|
+
}, 201);
|
|
167
|
+
}))));
|
|
168
|
+
yield* router.add("POST", `${ATTACHMENTS_PATH}/:id/complete`, asAgent((_, caller, runtime) => answered(Effect.gen(function* () {
|
|
169
|
+
const attachment = yield* runtime.complete(yield* idOf, caller);
|
|
170
|
+
return json({ attachment: attachmentForWire(attachment) });
|
|
171
|
+
}))));
|
|
172
|
+
yield* router.add("GET", ATTACHMENTS_PATH, asAgent((_, caller, runtime) => answered(Effect.gen(function* () {
|
|
173
|
+
const listed = yield* runtime.list(caller);
|
|
174
|
+
return json({ attachments: listed.map(attachmentForWire) });
|
|
175
|
+
}))));
|
|
176
|
+
yield* router.add("GET", `${ATTACHMENTS_PATH}/:id`, asAgent((_, caller, runtime) => answered(Effect.gen(function* () {
|
|
177
|
+
const { attachment, download } = yield* runtime.read(yield* idOf, caller);
|
|
178
|
+
return json({
|
|
179
|
+
attachment: attachmentForWire(attachment),
|
|
180
|
+
download: grantForWire(download),
|
|
181
|
+
});
|
|
182
|
+
}))));
|
|
183
|
+
yield* router.add("POST", `${ATTACHMENTS_PATH}/:id/discard`, asAgent((_, caller, runtime) => answered(Effect.gen(function* () {
|
|
184
|
+
const attachment = yield* runtime.discard(yield* idOf, caller);
|
|
185
|
+
return json({ attachment: attachmentForWire(attachment) });
|
|
186
|
+
}))));
|
|
187
|
+
yield* router.add("POST", `${ATTACHMENTS_PATH}/:id/promote`, asAgent((request, caller, runtime) => answered(Effect.gen(function* () {
|
|
188
|
+
const requestId = yield* RequestId;
|
|
189
|
+
const body = yield* decodeBody(Promotion, request);
|
|
190
|
+
if (!body.ok) {
|
|
191
|
+
return invalidBody(requestId, body.issues);
|
|
192
|
+
}
|
|
193
|
+
const promotion = body.value;
|
|
194
|
+
const promoted = yield* runtime.promote(yield* idOf, {
|
|
195
|
+
entity: promotion.entity,
|
|
196
|
+
action: promotion.action,
|
|
197
|
+
input: promotion.input ?? {},
|
|
198
|
+
...(promotion.field === undefined ? {} : { field: promotion.field }),
|
|
199
|
+
...(promotion.record_id === undefined ? {} : { recordId: promotion.record_id }),
|
|
200
|
+
}, caller, (call) => Effect.gen(function* () {
|
|
201
|
+
const planned = options.plan.entities.find((entity) => entity.name === call.entity);
|
|
202
|
+
const action = planned?.actions.find((item) => item.name === call.action);
|
|
203
|
+
if (planned === undefined || action === undefined) {
|
|
204
|
+
return yield* Effect.die(new Error("The promotion's action was not planned."));
|
|
205
|
+
}
|
|
206
|
+
// The action runs as the member, with the after-commit hooks and connections
|
|
207
|
+
// of any other call.
|
|
208
|
+
const answer = yield* operations
|
|
209
|
+
.runAction(planned, action, {
|
|
210
|
+
input: call.input,
|
|
211
|
+
validateOnly: false,
|
|
212
|
+
options: call.options,
|
|
213
|
+
identityToken: identityToken(request),
|
|
214
|
+
})
|
|
215
|
+
.pipe(Effect.provideService(CurrentPrincipal, caller.principal), Effect.provideService(RequestId, requestId));
|
|
216
|
+
return { result: answer.result, edits: answer.edits };
|
|
217
|
+
}));
|
|
218
|
+
return json({
|
|
219
|
+
attachment: attachmentForWire(promoted.attachment),
|
|
220
|
+
deduplicated: promoted.deduplicated,
|
|
221
|
+
result: promoted.result,
|
|
222
|
+
});
|
|
223
|
+
}))));
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause, Data, Effect, Layer, Option, Schema } from "effect";
|
|
1
|
+
import { Cause, Data, Effect, Layer, Option, Schema, SchemaTransformation } from "effect";
|
|
2
2
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
3
3
|
import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi";
|
|
4
4
|
import { encodeManifest } from "../../manifest.js";
|
|
@@ -6,7 +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 { describeCatalog, measuresFor, RECORDS_OR_AGGREGATES } from "../../render.js";
|
|
9
|
+
import { describeCatalog, measureDescription, measuresFor, RECORDS_OR_AGGREGATES, } from "../../render.js";
|
|
10
10
|
import { RequestId, errorBodyForWire, expectedFailure, internalError, schemaIssues, } from "../errors.js";
|
|
11
11
|
import { publicOrigin } from "../oauth/protocol.js";
|
|
12
12
|
import { mcpChallenge, mcpResource } from "../oauth/server.js";
|
|
@@ -14,7 +14,8 @@ import { identityToken, makeOperations, toQuery } from "../operations.js";
|
|
|
14
14
|
import { OntologyBackend } from "../runtime-contract.js";
|
|
15
15
|
import { runtimeCredential } from "../runtime-identity.js";
|
|
16
16
|
import { ActionResponse, HistoryPage, HistoryParams, IdParam, Me, } from "../schemas.js";
|
|
17
|
-
import {
|
|
17
|
+
import { uploadFields } from "../../storage/attachments.js";
|
|
18
|
+
import { ACTION_OPTIONS_ARGUMENT, mcpTools, visibleTools } from "./tools.js";
|
|
18
19
|
/** The URL path of the ontology's MCP server, outside `/api/ontology`: one per app. */
|
|
19
20
|
export const MCP_PATH = "/mcp";
|
|
20
21
|
/** The MCP revisions the server speaks, newest first. */
|
|
@@ -66,6 +67,23 @@ function decodeArguments(schema, args) {
|
|
|
66
67
|
onExcessProperty: "error",
|
|
67
68
|
}).pipe(Effect.mapError((error) => new InvalidArguments({ issues: schemaIssues(error.issue) })));
|
|
68
69
|
}
|
|
70
|
+
/** The `_options` argument of an action tool that consumes an upload. */
|
|
71
|
+
const McpActionOptions = Schema.Struct({
|
|
72
|
+
idempotency_key: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(128))),
|
|
73
|
+
create_intent: Schema.optionalKey(Schema.String.check(Schema.isUUID())),
|
|
74
|
+
}).pipe(Schema.decodeTo(Schema.Struct({
|
|
75
|
+
idempotencyKey: Schema.optionalKey(Schema.String),
|
|
76
|
+
createIntent: Schema.optionalKey(Schema.String),
|
|
77
|
+
}), SchemaTransformation.transform({
|
|
78
|
+
decode: (value) => ({
|
|
79
|
+
...(value.idempotency_key === undefined ? {} : { idempotencyKey: value.idempotency_key }),
|
|
80
|
+
...(value.create_intent === undefined ? {} : { createIntent: value.create_intent }),
|
|
81
|
+
}),
|
|
82
|
+
encode: (value) => ({
|
|
83
|
+
...(value.idempotencyKey === undefined ? {} : { idempotency_key: value.idempotencyKey }),
|
|
84
|
+
...(value.createIntent === undefined ? {} : { create_intent: value.createIntent }),
|
|
85
|
+
}),
|
|
86
|
+
})));
|
|
69
87
|
/** What the service sends: a value encoded with the schema of the matching HTTP route. */
|
|
70
88
|
function encoded(schema, value) {
|
|
71
89
|
return Schema.encodeUnknownEffect(schema)(value).pipe(Effect.orDie);
|
|
@@ -178,9 +196,14 @@ function runTool({ built, operations, aggregate }, tool, args, request) {
|
|
|
178
196
|
return structured(yield* encoded(HistoryPage, yield* operations.history(target.planned, id, params)));
|
|
179
197
|
}
|
|
180
198
|
case "action": {
|
|
199
|
+
const { [ACTION_OPTIONS_ARGUMENT]: rawOptions, ...input } = args;
|
|
200
|
+
const options = rawOptions === undefined || uploadFields(target.action.action.input).length === 0
|
|
201
|
+
? undefined
|
|
202
|
+
: yield* decodeArguments(McpActionOptions, rawOptions);
|
|
181
203
|
const answer = yield* operations.runAction(target.planned, target.action, {
|
|
182
|
-
input: args,
|
|
204
|
+
input: options === undefined ? args : input,
|
|
183
205
|
validateOnly: false,
|
|
206
|
+
...(options === undefined ? {} : { options }),
|
|
184
207
|
identityToken: identityToken(request),
|
|
185
208
|
});
|
|
186
209
|
return structured(yield* encoded(ActionResponse, answer));
|
|
@@ -264,7 +287,7 @@ function instructions({ built }, permissions) {
|
|
|
264
287
|
return (definition.source === undefined && isAllowed(permissionOf(name, READ_VERB), { permissions }));
|
|
265
288
|
})
|
|
266
289
|
.map(({ name, resolved }) => `- ${name}: ${resolved.entity.definition.description}`);
|
|
267
|
-
const measures = measuresFor(manifest, permissions).map(({ name, measure }) => `- ${name}: ${measure
|
|
290
|
+
const measures = measuresFor(manifest, permissions).map(({ name, entity, measure }) => `- ${name}: ${measureDescription(entity, measure)}`);
|
|
268
291
|
return [
|
|
269
292
|
"This server is the app's ontology: the records the company works with and the actions " +
|
|
270
293
|
"that change them. Every change goes through an action, is checked against your " +
|
|
@@ -43,6 +43,12 @@ export interface McpTool {
|
|
|
43
43
|
readonly inputSchemaFor?: (caller: PermissionHolder) => JsonSchemaObject | undefined;
|
|
44
44
|
readonly target: McpToolTarget;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The argument an action that consumes an upload takes beside its input: the idempotency key the
|
|
48
|
+
* runtime requires of it, and the create intent the upload was begun with. The service removes it
|
|
49
|
+
* before the input is validated.
|
|
50
|
+
*/
|
|
51
|
+
export declare const ACTION_OPTIONS_ARGUMENT = "_options";
|
|
46
52
|
/**
|
|
47
53
|
* The MCP tools of an ontology: `describe` and `me`, `aggregate` when the ontology has measures,
|
|
48
54
|
* then for every stored entity `find`, `get`,
|
|
@@ -5,10 +5,11 @@ import { isValueMatched } from "../../manifest.js";
|
|
|
5
5
|
import { GENERATED_UPSERT_PREFIX, humanize, mcpToolName } from "../../names.js";
|
|
6
6
|
import { isAllowed, permissionOf, READ_VERB } from "../../permissions.js";
|
|
7
7
|
import { queryJsonSchema } from "../../query.js";
|
|
8
|
-
import { measuresFor, RECORDS_OR_AGGREGATES } from "../../render.js";
|
|
8
|
+
import { measureDescription, measuresFor, RECORDS_OR_AGGREGATES, } from "../../render.js";
|
|
9
9
|
import { actionInputJsonSchema } from "../../runtime/generated-actions.js";
|
|
10
|
+
import { uploadFields } from "../../storage/attachments.js";
|
|
10
11
|
import { actionDescription, requires } from "../descriptions.js";
|
|
11
|
-
import { AGGREGATE_FILTER_OPERATORS, AGGREGATE_GRANULARITIES, AGGREGATE_LIMITS, HistoryPage, Me, } from "../schemas.js";
|
|
12
|
+
import { AGGREGATE_FILTER_OPERATORS, AGGREGATE_GRANULARITIES, AGGREGATE_LIMITS, AGGREGATE_PERIOD_UNITS, AGGREGATE_PERIODS, HistoryPage, MAXIMUM_TIMEZONE_LENGTH, Me, } from "../schemas.js";
|
|
12
13
|
const DEFS = "#/$defs/";
|
|
13
14
|
/**
|
|
14
15
|
* A JSON Schema whose root is an object schema, as MCP requires of tool inputs and outputs: a root
|
|
@@ -160,6 +161,37 @@ function readTools(planned, schemas, built) {
|
|
|
160
161
|
},
|
|
161
162
|
];
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* The argument an action that consumes an upload takes beside its input: the idempotency key the
|
|
166
|
+
* runtime requires of it, and the create intent the upload was begun with. The service removes it
|
|
167
|
+
* before the input is validated.
|
|
168
|
+
*/
|
|
169
|
+
export const ACTION_OPTIONS_ARGUMENT = "_options";
|
|
170
|
+
const ACTION_OPTIONS_SCHEMA = {
|
|
171
|
+
type: "object",
|
|
172
|
+
description: "Options of an action that stores an uploaded file: idempotency_key, required with an upload, " +
|
|
173
|
+
"and create_intent, the one the upload was begun with when it creates a record.",
|
|
174
|
+
properties: {
|
|
175
|
+
idempotency_key: { type: "string", minLength: 1, maxLength: 128 },
|
|
176
|
+
create_intent: { type: "string", format: "uuid" },
|
|
177
|
+
},
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
};
|
|
180
|
+
/** The input schema of an action tool; one that consumes an upload also takes its options. */
|
|
181
|
+
function actionToolInput(action) {
|
|
182
|
+
const schema = rootObject(actionInputJsonSchema(action.input));
|
|
183
|
+
const properties = schema.properties;
|
|
184
|
+
if (uploadFields(action.input).length === 0 ||
|
|
185
|
+
schema.type !== "object" ||
|
|
186
|
+
properties === undefined ||
|
|
187
|
+
ACTION_OPTIONS_ARGUMENT in properties) {
|
|
188
|
+
return schema;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
...schema,
|
|
192
|
+
properties: { ...properties, [ACTION_OPTIONS_ARGUMENT]: ACTION_OPTIONS_SCHEMA },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
163
195
|
function actionTool(planned, plannedAction) {
|
|
164
196
|
const { name, action, permissions } = plannedAction;
|
|
165
197
|
const hints = hintsOf(planned, plannedAction);
|
|
@@ -167,7 +199,7 @@ function actionTool(planned, plannedAction) {
|
|
|
167
199
|
name: mcpToolName(planned.name, name),
|
|
168
200
|
title: `${planned.resolved.entity.definition.label}: ${humanize(name).toLowerCase()}`,
|
|
169
201
|
description: actionDescription(plannedAction),
|
|
170
|
-
inputSchema:
|
|
202
|
+
inputSchema: actionToolInput(action),
|
|
171
203
|
annotations: {
|
|
172
204
|
readOnlyHint: false,
|
|
173
205
|
destructiveHint: hints.destructive,
|
|
@@ -211,11 +243,30 @@ const AGGREGATE_NAME = {
|
|
|
211
243
|
const AGGREGATE_VALUE = {
|
|
212
244
|
anyOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
|
|
213
245
|
};
|
|
246
|
+
/** `a, b or c`. */
|
|
247
|
+
function alternatives(values) {
|
|
248
|
+
return values.length < 2
|
|
249
|
+
? values.join("")
|
|
250
|
+
: `${values.slice(0, -1).join(", ")} or ${values[values.length - 1]}`;
|
|
251
|
+
}
|
|
252
|
+
const PERIODS = `${alternatives(AGGREGATE_PERIODS)}; or last N ${alternatives(AGGREGATE_PERIOD_UNITS)}, N at ` +
|
|
253
|
+
"least 1. Write a period exactly so: lowercase, single spaces, plural units even for 1 (last 1 " +
|
|
254
|
+
"days). last N days ends yesterday; last N weeks, months, quarters or years are whole units " +
|
|
255
|
+
"before the current one. Weeks start on Monday.";
|
|
256
|
+
const FILTER_OPERATOR_DESCRIPTION = "equals, notEquals, gt, gte, lt, lte, contains, startsWith, endsWith and inPeriod take value; " +
|
|
257
|
+
"in, notIn and between take values; isNull and isNotNull take neither. notEquals and notIn also " +
|
|
258
|
+
"match null values and rows a route finds nothing for, so equals and notEquals together cover " +
|
|
259
|
+
"every row.";
|
|
260
|
+
const FILTER_VALUE_DESCRIPTION = "equals, notEquals: the value. gt, gte, lt, lte: the bound, of a date, timestamp or number " +
|
|
261
|
+
"dimension; a date covers whole days on a timestamp. contains, startsWith, endsWith: a " +
|
|
262
|
+
"non-empty string, matched case-sensitively, without wildcards. inPeriod: a date or timestamp " +
|
|
263
|
+
`dimension's period, today being the date in the request's timezone: ${PERIODS}`;
|
|
214
264
|
/** One `anyOf` branch per measure: a JSON Schema `enum` cannot describe its values. */
|
|
215
265
|
function measureChoice({ name, entity, measure }) {
|
|
216
266
|
return {
|
|
217
267
|
const: name,
|
|
218
|
-
description: `${entity.label} · ${measure.label} (${measure.unit}):
|
|
268
|
+
description: `${entity.label} · ${measure.label} (${measure.unit}): ` +
|
|
269
|
+
measureDescription(entity, measure),
|
|
219
270
|
};
|
|
220
271
|
}
|
|
221
272
|
/** The aggregate tool's input for a caller: the request of `POST /api/ontology/aggregate`. */
|
|
@@ -248,15 +299,18 @@ function aggregateInput(measures) {
|
|
|
248
299
|
type: "object",
|
|
249
300
|
properties: {
|
|
250
301
|
dimension: AGGREGATE_NAME,
|
|
251
|
-
operator: {
|
|
252
|
-
|
|
302
|
+
operator: {
|
|
303
|
+
enum: [...AGGREGATE_FILTER_OPERATORS],
|
|
304
|
+
description: FILTER_OPERATOR_DESCRIPTION,
|
|
305
|
+
},
|
|
306
|
+
value: { ...AGGREGATE_VALUE, description: FILTER_VALUE_DESCRIPTION },
|
|
253
307
|
values: {
|
|
254
308
|
type: "array",
|
|
255
309
|
minItems: 1,
|
|
256
310
|
maxItems: AGGREGATE_LIMITS.values,
|
|
257
311
|
items: AGGREGATE_VALUE,
|
|
258
|
-
description: "The values of in, or the two inclusive bounds of between, for example " +
|
|
259
|
-
'["2026-09-01", "2026-09-30"].',
|
|
312
|
+
description: "The values of in and notIn, or the two inclusive bounds of between, for example " +
|
|
313
|
+
'["2026-09-01", "2026-09-30"]; dates cover whole days on a timestamp.',
|
|
260
314
|
},
|
|
261
315
|
},
|
|
262
316
|
required: ["dimension", "operator"],
|
|
@@ -265,9 +319,17 @@ function aggregateInput(measures) {
|
|
|
265
319
|
},
|
|
266
320
|
granularity: {
|
|
267
321
|
enum: [...AGGREGATE_GRANULARITIES],
|
|
268
|
-
description: "Buckets the date or timestamp dimension granularityOf names
|
|
322
|
+
description: "Buckets the date or timestamp dimension granularityOf names; hour applies to " +
|
|
323
|
+
"timestamps only. Weeks start on Monday.",
|
|
269
324
|
},
|
|
270
325
|
granularityOf: { ...AGGREGATE_NAME, description: "A requested date or timestamp dimension." },
|
|
326
|
+
timezone: {
|
|
327
|
+
type: "string",
|
|
328
|
+
maxLength: MAXIMUM_TIMEZONE_LENGTH,
|
|
329
|
+
description: "An IANA timezone, such as Europe/Moscow, or UTC. Buckets of timestamps, date bounds on " +
|
|
330
|
+
"timestamps and inPeriod's today follow it; without it, the timezone of the entity that " +
|
|
331
|
+
"owns the dimension.",
|
|
332
|
+
},
|
|
271
333
|
orderBy: {
|
|
272
334
|
type: "array",
|
|
273
335
|
maxItems: AGGREGATE_LIMITS.orderBy,
|
|
@@ -301,9 +363,14 @@ const AGGREGATE_DESCRIPTION = "Numbers about the model: measures from one or sev
|
|
|
301
363
|
'"values": ["2026-09-01", "2026-09-30"]}]}\n' +
|
|
302
364
|
'- September units sold beside units in stock, by status: {"measures": ["sales.units", ' +
|
|
303
365
|
'"stock.on_hand"], "dimensions": ["product.status"], "filters": [{"dimension": ' +
|
|
304
|
-
'"sales.order_date", "operator": "between", "values": ["2026-09-01", "2026-09-30"]}]}\n
|
|
305
|
-
|
|
306
|
-
"
|
|
366
|
+
'"sales.order_date", "operator": "between", "values": ["2026-09-01", "2026-09-30"]}]}\n' +
|
|
367
|
+
'- Won orders in the last 4 weeks by week: {"measures": ["sales.won_orders"], "dimensions": ' +
|
|
368
|
+
'["sales.order_date"], "granularity": "week", "granularityOf": "sales.order_date", "filters": ' +
|
|
369
|
+
'[{"dimension": "sales.order_date", "operator": "inPeriod", "value": "last 4 weeks"}]}\n\n' +
|
|
370
|
+
"Partitioned warehouse entities require a between or inPeriod filter on their date property; " +
|
|
371
|
+
"the error names it. Currency measures need a single-currency filter, unless the measure is " +
|
|
372
|
+
"pinned to one currency. Use inPeriod for relative dates; the answer's execution.periods says " +
|
|
373
|
+
"which dates were used. " +
|
|
307
374
|
"Answers come from the warehouse, and stored records from its replica: execution.sources says " +
|
|
308
375
|
"how fresh each is, and execution.warnings says when a filter did not apply to a measure. " +
|
|
309
376
|
"Requires reading every entity the request reads, path intermediates included. Call describe " +
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context, Schema, type Effect, type Layer, type Redacted } from "effect";
|
|
2
|
+
import type { AttachmentRuntime } from "../storage/attachments.js";
|
|
2
3
|
import type { BlobRuntime } from "../storage/sessions.js";
|
|
3
4
|
import type { BlobStorageBinding } from "../storage/provider.js";
|
|
4
5
|
import type { AnyAction } from "../action.js";
|
|
@@ -247,6 +248,8 @@ export interface AgentStore {
|
|
|
247
248
|
}
|
|
248
249
|
declare const OntologyBackend_base: Context.ServiceClass<OntologyBackend, "@segmentstream-cloud/ontology/OntologyBackend", {
|
|
249
250
|
readonly blobs?: BlobRuntime;
|
|
251
|
+
/** The agent's temporary attachments, on deployments with file storage. */
|
|
252
|
+
readonly attachments?: AttachmentRuntime;
|
|
250
253
|
readonly maintainBlobs?: Effect.Effect<{
|
|
251
254
|
readonly removed: number;
|
|
252
255
|
}, unknown>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { AGGREGATE_FILTER_OPERATORS, ENTITY_AGGREGATE_FILTER_OPERATORS, type AggregateValue } from "../aggregate-filters.js";
|
|
2
3
|
import type { ApiPlan } from "./plan.js";
|
|
4
|
+
export { AGGREGATE_FILTER_OPERATORS, AGGREGATE_PERIOD_UNITS, AGGREGATE_PERIODS, ENTITY_AGGREGATE_FILTER_OPERATORS, } from "../aggregate-filters.js";
|
|
3
5
|
/** A record id in a path: a UUID. */
|
|
4
6
|
export declare const IdParam: Schema.String;
|
|
5
7
|
/** The schemas of one entity's records and queries. */
|
|
@@ -149,6 +151,17 @@ export declare const ActionResponse: Schema.Struct<{
|
|
|
149
151
|
}>;
|
|
150
152
|
export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
151
153
|
readonly entities: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
154
|
+
readonly measures: Schema.optionalKey<Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
155
|
+
readonly name: Schema.String;
|
|
156
|
+
readonly label: Schema.String;
|
|
157
|
+
readonly description: Schema.String;
|
|
158
|
+
readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
|
|
159
|
+
readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
|
|
160
|
+
readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
161
|
+
readonly balanceOver: Schema.optionalKey<Schema.String>;
|
|
162
|
+
}>, {
|
|
163
|
+
readonly balanceOver: "balance_over";
|
|
164
|
+
}>>>;
|
|
152
165
|
readonly properties: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
153
166
|
readonly column: Schema.optionalKey<Schema.String>;
|
|
154
167
|
readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
@@ -201,14 +214,6 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
201
214
|
readonly key: Schema.$Array<Schema.String>;
|
|
202
215
|
readonly currency: Schema.optionalKey<Schema.String>;
|
|
203
216
|
}>>;
|
|
204
|
-
readonly measures: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
205
|
-
readonly name: Schema.String;
|
|
206
|
-
readonly label: Schema.String;
|
|
207
|
-
readonly description: Schema.String;
|
|
208
|
-
readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
|
|
209
|
-
readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
|
|
210
|
-
readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
211
|
-
}>>>;
|
|
212
217
|
readonly id: Schema.String;
|
|
213
218
|
readonly name: Schema.String;
|
|
214
219
|
readonly label: Schema.String;
|
|
@@ -264,6 +269,17 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
264
269
|
}>>>;
|
|
265
270
|
}>, Schema.Struct<{
|
|
266
271
|
readonly entities: Schema.toEncoded<Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
272
|
+
readonly measures: Schema.optionalKey<Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
273
|
+
readonly name: Schema.String;
|
|
274
|
+
readonly label: Schema.String;
|
|
275
|
+
readonly description: Schema.String;
|
|
276
|
+
readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
|
|
277
|
+
readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
|
|
278
|
+
readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
279
|
+
readonly balanceOver: Schema.optionalKey<Schema.String>;
|
|
280
|
+
}>, {
|
|
281
|
+
readonly balanceOver: "balance_over";
|
|
282
|
+
}>>>;
|
|
267
283
|
readonly properties: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
268
284
|
readonly column: Schema.optionalKey<Schema.String>;
|
|
269
285
|
readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
@@ -316,14 +332,6 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
316
332
|
readonly key: Schema.$Array<Schema.String>;
|
|
317
333
|
readonly currency: Schema.optionalKey<Schema.String>;
|
|
318
334
|
}>>;
|
|
319
|
-
readonly measures: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
320
|
-
readonly name: Schema.String;
|
|
321
|
-
readonly label: Schema.String;
|
|
322
|
-
readonly description: Schema.String;
|
|
323
|
-
readonly unit: Schema.Literals<readonly ["number", "currency", "ratio", "percent"]>;
|
|
324
|
-
readonly format: Schema.Literals<readonly ["decimal", "currency", "percent"]>;
|
|
325
|
-
readonly expression: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
326
|
-
}>>>;
|
|
327
335
|
readonly id: Schema.String;
|
|
328
336
|
readonly name: Schema.String;
|
|
329
337
|
readonly label: Schema.String;
|
|
@@ -430,11 +438,12 @@ export declare const AGGREGATE_LIMITS: {
|
|
|
430
438
|
readonly orderBy: 50;
|
|
431
439
|
readonly rows: 1000;
|
|
432
440
|
};
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
export declare const AGGREGATE_GRANULARITIES: readonly ["day", "week", "month"];
|
|
437
|
-
|
|
441
|
+
/**
|
|
442
|
+
* The granularities a date or timestamp dimension buckets by; `hour` applies to timestamps only.
|
|
443
|
+
*/
|
|
444
|
+
export declare const AGGREGATE_GRANULARITIES: readonly ["hour", "day", "week", "month", "quarter", "year"];
|
|
445
|
+
/** The longest `timezone` a request names. */
|
|
446
|
+
export declare const MAXIMUM_TIMEZONE_LENGTH = 64;
|
|
438
447
|
interface AggregateFilter<Operator extends string> {
|
|
439
448
|
readonly dimension: string;
|
|
440
449
|
readonly operator: Operator;
|
|
@@ -458,6 +467,7 @@ export interface AggregateRequest {
|
|
|
458
467
|
readonly filters?: ReadonlyArray<AggregateFilter<(typeof AGGREGATE_FILTER_OPERATORS)[number]>>;
|
|
459
468
|
readonly granularity?: (typeof AGGREGATE_GRANULARITIES)[number];
|
|
460
469
|
readonly granularityOf?: string;
|
|
470
|
+
readonly timezone?: string;
|
|
461
471
|
readonly orderBy?: ReadonlyArray<AggregateOrdering>;
|
|
462
472
|
readonly limit?: number;
|
|
463
473
|
}
|
|
@@ -476,6 +486,7 @@ export interface EntityAggregateRequest {
|
|
|
476
486
|
readonly filters?: ReadonlyArray<AggregateFilter<(typeof ENTITY_AGGREGATE_FILTER_OPERATORS)[number]>>;
|
|
477
487
|
readonly granularity?: (typeof AGGREGATE_GRANULARITIES)[number];
|
|
478
488
|
readonly granularityOf?: string;
|
|
489
|
+
readonly timezone?: string;
|
|
479
490
|
readonly orderBy?: ReadonlyArray<AggregateOrdering>;
|
|
480
491
|
readonly limit?: number;
|
|
481
492
|
}
|
|
@@ -491,6 +502,7 @@ export declare const AggregateResult: Schema.Struct<{
|
|
|
491
502
|
readonly implicit: Schema.optionalKey<Schema.Boolean>;
|
|
492
503
|
readonly additive: Schema.optionalKey<Schema.Boolean>;
|
|
493
504
|
readonly filters: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
505
|
+
readonly balanceOver: Schema.optionalKey<Schema.String>;
|
|
494
506
|
}>>;
|
|
495
507
|
readonly rows: Schema.$Array<Schema.$Record<Schema.String, Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Boolean]>>>>;
|
|
496
508
|
readonly truncated: Schema.Boolean;
|
|
@@ -503,6 +515,13 @@ export declare const AggregateResult: Schema.Struct<{
|
|
|
503
515
|
readonly entity: Schema.String;
|
|
504
516
|
readonly asOf: Schema.String;
|
|
505
517
|
}>>>;
|
|
518
|
+
readonly periods: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
519
|
+
readonly dimension: Schema.String;
|
|
520
|
+
readonly value: Schema.String;
|
|
521
|
+
readonly from: Schema.String;
|
|
522
|
+
readonly to: Schema.String;
|
|
523
|
+
readonly timezone: Schema.String;
|
|
524
|
+
}>>>;
|
|
506
525
|
}>;
|
|
507
526
|
readonly deploymentId: Schema.NullOr<Schema.String>;
|
|
508
527
|
readonly definitionDigest: Schema.String;
|
|
@@ -549,4 +568,3 @@ export declare const ErrorSchemas: {
|
|
|
549
568
|
connectionResponseInvalid: Schema.Top;
|
|
550
569
|
connectionUnavailable: Schema.Top;
|
|
551
570
|
};
|
|
552
|
-
export {};
|