@segmentstream-cloud/ontology 0.18.2 → 0.20.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/action.d.ts +8 -5
- package/dist/analyze.js +2 -2
- package/dist/manifest.d.ts +12 -5
- package/dist/manifest.js +2 -1
- package/dist/migrate/model.d.ts +2 -0
- package/dist/service/agent/conversation-routes.d.ts +14 -0
- package/dist/service/agent/conversation-routes.js +43 -0
- package/dist/service/agent/conversations.d.ts +35 -0
- package/dist/service/agent/conversations.js +68 -0
- package/dist/service/errors.js +11 -0
- package/dist/service/mcp/server.d.ts +2 -0
- package/dist/service/mcp/server.js +32 -5
- package/dist/service/mcp/tools.d.ts +5 -1
- package/dist/service/mcp/tools.js +60 -9
- package/dist/service/schemas.d.ts +2 -0
- package/dist/service/service.js +12 -1
- package/package.json +1 -1
package/dist/action.d.ts
CHANGED
|
@@ -18,13 +18,15 @@ export interface ActionContext<Input, Prepared = undefined> extends PrepareConte
|
|
|
18
18
|
/** What `prepare` may fail with: the action errors, and connection calls that failed. */
|
|
19
19
|
export type PrepareError = ActionError | ConnectionError;
|
|
20
20
|
/**
|
|
21
|
-
* How an action behaves
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* How an action behaves. Agents confirm destructive or irreversible actions before preparation.
|
|
22
|
+
* Custom actions without safety labels require confirmation for backwards compatibility.
|
|
23
|
+
* Idempotence does not decide whether confirmation is needed.
|
|
24
24
|
*/
|
|
25
25
|
export interface ActionHints {
|
|
26
|
-
/** Whether the action
|
|
26
|
+
/** Whether the action destroys records or established decisions. Unlabelled actions default true. */
|
|
27
27
|
readonly destructive?: boolean;
|
|
28
|
+
/** Whether the effect cannot be fully undone, such as sending a notification. Default false. */
|
|
29
|
+
readonly irreversible?: boolean;
|
|
28
30
|
/** Whether running it again with the same input changes nothing more. Default false. */
|
|
29
31
|
readonly idempotent?: boolean;
|
|
30
32
|
}
|
|
@@ -79,7 +81,8 @@ interface ActionOptions<Input extends Schema.Top> {
|
|
|
79
81
|
readonly permission?: string;
|
|
80
82
|
/**
|
|
81
83
|
* How the action behaves, for the agents that run it over MCP. Set `destructive: false` for an
|
|
82
|
-
* action
|
|
84
|
+
* routine reversible action, `irreversible: true` for an effect without a complete undo, and
|
|
85
|
+
* `idempotent: true` for one
|
|
83
86
|
* that a repeated call leaves as it was.
|
|
84
87
|
*/
|
|
85
88
|
readonly hints?: ActionHints;
|
package/dist/analyze.js
CHANGED
|
@@ -641,8 +641,8 @@ function resolveActions(candidate, scope) {
|
|
|
641
641
|
}
|
|
642
642
|
return actions;
|
|
643
643
|
}
|
|
644
|
-
const HINT_NAMES = ["destructive", "idempotent"];
|
|
645
|
-
/** The action's `hints`: an object of booleans, `destructive` and `idempotent`. */
|
|
644
|
+
const HINT_NAMES = ["destructive", "irreversible", "idempotent"];
|
|
645
|
+
/** The action's `hints`: an object of booleans, `destructive`, `irreversible` and `idempotent`. */
|
|
646
646
|
function resolveHints(path, action, report) {
|
|
647
647
|
const declared = action.hints;
|
|
648
648
|
if (declared === undefined) {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -108,10 +108,11 @@ export declare const ActionManifest: Schema.Struct<{
|
|
|
108
108
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
109
109
|
/**
|
|
110
110
|
* How the action behaves, as MCP tool annotations tell agents. Written only when the action
|
|
111
|
-
* declares hints; absent
|
|
111
|
+
* declares hints; absent safety labels require agent confirmation for compatibility.
|
|
112
112
|
*/
|
|
113
113
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
114
114
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
115
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
115
116
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
116
117
|
}>>;
|
|
117
118
|
}>;
|
|
@@ -244,10 +245,11 @@ export declare const EntityManifest: Schema.Struct<{
|
|
|
244
245
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
245
246
|
/**
|
|
246
247
|
* How the action behaves, as MCP tool annotations tell agents. Written only when the action
|
|
247
|
-
* declares hints; absent
|
|
248
|
+
* declares hints; absent safety labels require agent confirmation for compatibility.
|
|
248
249
|
*/
|
|
249
250
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
250
251
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
252
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
251
253
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
252
254
|
}>>;
|
|
253
255
|
}>>;
|
|
@@ -391,10 +393,11 @@ export declare const OntologyManifestWire: Schema.encodeKeys<Schema.Struct<{
|
|
|
391
393
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
392
394
|
/**
|
|
393
395
|
* How the action behaves, as MCP tool annotations tell agents. Written only when the action
|
|
394
|
-
* declares hints; absent
|
|
396
|
+
* declares hints; absent safety labels require agent confirmation for compatibility.
|
|
395
397
|
*/
|
|
396
398
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
397
399
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
400
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
398
401
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
399
402
|
}>>;
|
|
400
403
|
}>>;
|
|
@@ -541,10 +544,11 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
|
|
|
541
544
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
542
545
|
/**
|
|
543
546
|
* How the action behaves, as MCP tool annotations tell agents. Written only when the action
|
|
544
|
-
* declares hints; absent
|
|
547
|
+
* declares hints; absent safety labels require agent confirmation for compatibility.
|
|
545
548
|
*/
|
|
546
549
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
547
550
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
551
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
548
552
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
549
553
|
}>>;
|
|
550
554
|
}>>;
|
|
@@ -690,10 +694,11 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
|
|
|
690
694
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
691
695
|
/**
|
|
692
696
|
* How the action behaves, as MCP tool annotations tell agents. Written only when the action
|
|
693
|
-
* declares hints; absent
|
|
697
|
+
* declares hints; absent safety labels require agent confirmation for compatibility.
|
|
694
698
|
*/
|
|
695
699
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
696
700
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
701
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
697
702
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
698
703
|
}>>;
|
|
699
704
|
}>>;
|
|
@@ -789,6 +794,7 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
|
|
|
789
794
|
readonly connections?: readonly string[] | undefined;
|
|
790
795
|
readonly hints?: {
|
|
791
796
|
readonly destructive?: boolean | undefined;
|
|
797
|
+
readonly irreversible?: boolean | undefined;
|
|
792
798
|
readonly idempotent?: boolean | undefined;
|
|
793
799
|
} | undefined;
|
|
794
800
|
}[];
|
|
@@ -894,6 +900,7 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
|
|
|
894
900
|
readonly connections?: readonly string[] | undefined;
|
|
895
901
|
readonly hints?: {
|
|
896
902
|
readonly destructive?: boolean | undefined;
|
|
903
|
+
readonly irreversible?: boolean | undefined;
|
|
897
904
|
readonly idempotent?: boolean | undefined;
|
|
898
905
|
} | undefined;
|
|
899
906
|
}[];
|
package/dist/manifest.js
CHANGED
|
@@ -104,10 +104,11 @@ export const ActionManifest = Schema.Struct({
|
|
|
104
104
|
connections: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
105
105
|
/**
|
|
106
106
|
* How the action behaves, as MCP tool annotations tell agents. Written only when the action
|
|
107
|
-
* declares hints; absent
|
|
107
|
+
* declares hints; absent safety labels require agent confirmation for compatibility.
|
|
108
108
|
*/
|
|
109
109
|
hints: Schema.optionalKey(Schema.Struct({
|
|
110
110
|
destructive: Schema.optionalKey(Schema.Boolean),
|
|
111
|
+
irreversible: Schema.optionalKey(Schema.Boolean),
|
|
111
112
|
idempotent: Schema.optionalKey(Schema.Boolean),
|
|
112
113
|
})),
|
|
113
114
|
});
|
package/dist/migrate/model.d.ts
CHANGED
|
@@ -128,6 +128,7 @@ export declare const MigrationPlan: Schema.Struct<{
|
|
|
128
128
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
129
129
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
130
130
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
131
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
131
132
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
132
133
|
}>>;
|
|
133
134
|
}>>;
|
|
@@ -240,6 +241,7 @@ export declare const MigrationPlan: Schema.Struct<{
|
|
|
240
241
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
241
242
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
242
243
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
244
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
243
245
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
244
246
|
}>>;
|
|
245
247
|
}>>;
|
|
@@ -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
|
+
}
|
package/dist/service/errors.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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);
|
|
@@ -6,6 +6,8 @@ import type { PlannedAction, PlannedEntity } from "../plan.js";
|
|
|
6
6
|
export interface McpToolAnnotations {
|
|
7
7
|
readonly readOnlyHint: boolean;
|
|
8
8
|
readonly destructiveHint?: boolean;
|
|
9
|
+
/** Ontology extension: whether the action has an effect without a complete undo. */
|
|
10
|
+
readonly irreversibleHint?: boolean;
|
|
9
11
|
readonly idempotentHint?: boolean;
|
|
10
12
|
/** Whether the tool reaches beyond the ontology: true for actions that call connections. */
|
|
11
13
|
readonly openWorldHint: boolean;
|
|
@@ -24,6 +26,8 @@ export type McpToolTarget = {
|
|
|
24
26
|
readonly kind: "me";
|
|
25
27
|
} | {
|
|
26
28
|
readonly kind: "aggregate";
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "conversation-search";
|
|
27
31
|
};
|
|
28
32
|
/** A tool of the ontology's MCP server, derived from the same plan as the HTTP routes. */
|
|
29
33
|
export interface McpTool {
|
|
@@ -56,7 +60,7 @@ export declare const ACTION_OPTIONS_ARGUMENT = "_options";
|
|
|
56
60
|
* as they have no record routes. Throws {@link InvalidOntology} when two tools share a name, which
|
|
57
61
|
* `check` already refuses.
|
|
58
62
|
*/
|
|
59
|
-
export declare function mcpTools(built: BuiltApi): ReadonlyArray<McpTool>;
|
|
63
|
+
export declare function mcpTools(built: BuiltApi, conversations?: boolean): ReadonlyArray<McpTool>;
|
|
60
64
|
/**
|
|
61
65
|
* The tools a caller may see: those whose every permission the caller's roles grant, each with the
|
|
62
66
|
* caller's input schema.
|
|
@@ -65,22 +65,28 @@ function historyInput(label) {
|
|
|
65
65
|
const READ = { readOnlyHint: true, openWorldHint: false };
|
|
66
66
|
/** The hints of generated actions, which only their name tells apart. */
|
|
67
67
|
const GENERATED_HINTS = {
|
|
68
|
-
create: { destructive: false, idempotent: false },
|
|
69
|
-
update: { destructive:
|
|
70
|
-
delete: { destructive: true, idempotent: true },
|
|
71
|
-
link: { destructive: false, idempotent: true },
|
|
72
|
-
unlink: { destructive:
|
|
68
|
+
create: { destructive: false, irreversible: false, idempotent: false },
|
|
69
|
+
update: { destructive: false, irreversible: false, idempotent: true },
|
|
70
|
+
delete: { destructive: true, irreversible: false, idempotent: true },
|
|
71
|
+
link: { destructive: false, irreversible: false, idempotent: true },
|
|
72
|
+
unlink: { destructive: false, irreversible: false, idempotent: true },
|
|
73
73
|
};
|
|
74
74
|
/** MCP's defaults for a tool that writes: it may destroy, and repeating it may change more. */
|
|
75
|
-
const AUTHORED_DEFAULT = {
|
|
75
|
+
const AUTHORED_DEFAULT = {
|
|
76
|
+
destructive: true,
|
|
77
|
+
irreversible: false,
|
|
78
|
+
idempotent: false,
|
|
79
|
+
};
|
|
76
80
|
function hintsOf(planned, { name, generated }) {
|
|
77
81
|
if (generated) {
|
|
78
82
|
return name.startsWith(GENERATED_UPSERT_PREFIX)
|
|
79
|
-
? { destructive:
|
|
83
|
+
? { destructive: false, irreversible: false, idempotent: true }
|
|
80
84
|
: (GENERATED_HINTS[name] ?? AUTHORED_DEFAULT);
|
|
81
85
|
}
|
|
82
86
|
const declared = planned.resolved.actions.find((candidate) => candidate.name === name)?.hints;
|
|
83
|
-
|
|
87
|
+
// A safety label classifies the action; idempotence alone does not opt out of confirmation.
|
|
88
|
+
const classified = declared?.destructive !== undefined || declared?.irreversible !== undefined;
|
|
89
|
+
return { ...AUTHORED_DEFAULT, ...(classified ? { destructive: false } : {}), ...declared };
|
|
84
90
|
}
|
|
85
91
|
/** How an agent reaches the records a relationship leads to, with the tools it has. */
|
|
86
92
|
function relatedHint(planned, traversal) {
|
|
@@ -208,6 +214,7 @@ function actionTool(planned, plannedAction) {
|
|
|
208
214
|
annotations: {
|
|
209
215
|
readOnlyHint: false,
|
|
210
216
|
destructiveHint: hints.destructive,
|
|
217
|
+
irreversibleHint: hints.irreversible,
|
|
211
218
|
idempotentHint: hints.idempotent,
|
|
212
219
|
openWorldHint: action.connections.length > 0,
|
|
213
220
|
},
|
|
@@ -402,7 +409,7 @@ function aggregateTool(manifest) {
|
|
|
402
409
|
* as they have no record routes. Throws {@link InvalidOntology} when two tools share a name, which
|
|
403
410
|
* `check` already refuses.
|
|
404
411
|
*/
|
|
405
|
-
export function mcpTools(built) {
|
|
412
|
+
export function mcpTools(built, conversations = false) {
|
|
406
413
|
const { manifest } = built.plan.ontology;
|
|
407
414
|
const tools = [
|
|
408
415
|
...GLOBAL_TOOLS,
|
|
@@ -416,6 +423,50 @@ export function mcpTools(built) {
|
|
|
416
423
|
tools.push(...readTools(planned, schemas, built));
|
|
417
424
|
tools.push(...planned.actions.map((action) => actionTool(planned, action)));
|
|
418
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
|
+
}
|
|
419
470
|
const seen = new Set();
|
|
420
471
|
const duplicates = tools.filter(({ name }) => seen.size === seen.add(name).size);
|
|
421
472
|
if (duplicates.length > 0) {
|
|
@@ -235,6 +235,7 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
235
235
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
236
236
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
237
237
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
238
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
238
239
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
239
240
|
}>>;
|
|
240
241
|
}>>;
|
|
@@ -353,6 +354,7 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
353
354
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
354
355
|
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
355
356
|
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
357
|
+
readonly irreversible: Schema.optionalKey<Schema.Boolean>;
|
|
356
358
|
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
357
359
|
}>>;
|
|
358
360
|
}>>;
|
package/dist/service/service.js
CHANGED
|
@@ -5,6 +5,8 @@ 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 { platformConversations } from "./agent/conversations.js";
|
|
9
|
+
import { conversationRoutes } from "./agent/conversation-routes.js";
|
|
8
10
|
import { deployedAgentChannel } from "./agent/channel.js";
|
|
9
11
|
import { agentLayer } from "./agent/server.js";
|
|
10
12
|
import { makeAggregates } from "./aggregate.js";
|
|
@@ -95,6 +97,7 @@ export function createOntologyService(ontology, config) {
|
|
|
95
97
|
const agentChannel = config.runtimeIdentity === undefined || config.agentUrl === undefined
|
|
96
98
|
? undefined
|
|
97
99
|
: deployedAgentChannel(config.agentUrl);
|
|
100
|
+
const conversations = config.runtimeIdentity === undefined ? undefined : platformConversations(config);
|
|
98
101
|
const aggregates = makeAggregates({
|
|
99
102
|
manifest: ontology.manifest,
|
|
100
103
|
transport: platformAggregates({ ...config, manifestHash: ontology.manifest.hash }),
|
|
@@ -115,6 +118,7 @@ export function createOntologyService(ontology, config) {
|
|
|
115
118
|
replicate,
|
|
116
119
|
aggregates,
|
|
117
120
|
agentChannel,
|
|
121
|
+
conversations,
|
|
118
122
|
}), oauthLayer({ verifyToken }));
|
|
119
123
|
const agent = agentLayer({ verifyToken, verifyRuntime, exchangeSession });
|
|
120
124
|
const attachments = attachmentsLayer({
|
|
@@ -126,7 +130,14 @@ export function createOntologyService(ontology, config) {
|
|
|
126
130
|
verifyToken,
|
|
127
131
|
verifyRuntime,
|
|
128
132
|
});
|
|
129
|
-
const application = Layer.mergeAll(routes, mcp, agent, attachments,
|
|
133
|
+
const application = Layer.mergeAll(routes, mcp, agent, attachments, conversations === undefined
|
|
134
|
+
? Layer.empty
|
|
135
|
+
: conversationRoutes({
|
|
136
|
+
transport: conversations,
|
|
137
|
+
verifyToken,
|
|
138
|
+
verifyRuntime,
|
|
139
|
+
exchangeSession,
|
|
140
|
+
}), DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
|
|
130
141
|
const web = HttpRouter.toWebHandler(application, { disableLogger: true });
|
|
131
142
|
const hash = ontology.manifest.hash;
|
|
132
143
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/ontology",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|