ankka 0.9.1 → 0.11.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/_proto/ankka/protocol/v1/agent_pb.d.ts +35 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.js +8 -1
- package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +542 -2
- package/dist/_proto/ankka/protocol/v1/client_pb.js +107 -6
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +52 -1
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +330 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +81 -16
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +184 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +26 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +63 -0
- package/dist/_proto/ankka/protocol/v1/view_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +7 -0
- package/dist/_proto/ankka/protocol/v1/workflow_pb.js +1 -1
- package/dist/agent.d.ts +15 -1
- package/dist/agent.js +20 -0
- package/dist/approvals.d.ts +47 -0
- package/dist/approvals.js +57 -0
- package/dist/autonomous.d.ts +23 -1
- package/dist/autonomous.js +30 -1
- package/dist/client.d.ts +92 -4
- package/dist/client.js +262 -15
- package/dist/consumer.d.ts +40 -2
- package/dist/consumer.js +50 -0
- package/dist/context.d.ts +4 -0
- package/dist/contract.d.ts +36 -0
- package/dist/contract.js +58 -0
- package/dist/effects/keyed.d.ts +33 -0
- package/dist/effects/keyed.js +41 -0
- package/dist/effects/stateless.d.ts +21 -0
- package/dist/effects/stateless.js +19 -0
- package/dist/endpoint.d.ts +10 -1
- package/dist/endpoint.js +20 -0
- package/dist/graph.d.ts +171 -0
- package/dist/graph.js +446 -0
- package/dist/handlers.d.ts +10 -2
- package/dist/handlers.js +6 -2
- package/dist/index.d.ts +15 -6
- package/dist/index.js +13 -4
- package/dist/keyedView.d.ts +56 -0
- package/dist/keyedView.js +67 -0
- package/dist/mcp.d.ts +57 -0
- package/dist/mcp.js +64 -0
- package/dist/routes.d.ts +23 -3
- package/dist/routes.js +16 -3
- package/dist/server/agent.js +12 -4
- package/dist/server/discovery.d.ts +12 -1
- package/dist/server/discovery.js +95 -1
- package/dist/server/http.d.ts +2 -1
- package/dist/server/http.js +78 -4
- package/dist/server/stateless.js +104 -16
- package/dist/server/workflow.js +5 -1
- package/dist/service.d.ts +45 -3
- package/dist/service.js +234 -12
- package/dist/services.d.ts +137 -0
- package/dist/services.js +259 -0
- package/dist/socket.d.ts +15 -0
- package/dist/socket.js +25 -0
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +87 -14
- package/dist/startFrom.d.ts +32 -0
- package/dist/startFrom.js +44 -0
- package/dist/testkit/index.d.ts +2 -2
- package/dist/testkit/index.js +1 -1
- package/dist/testkit/integration.js +5 -0
- package/dist/testkit/kinds.d.ts +75 -5
- package/dist/testkit/kinds.js +264 -34
- package/dist/testkit/unit.d.ts +14 -1
- package/dist/testkit/unit.js +40 -3
- package/dist/timedAction.d.ts +17 -2
- package/dist/timedAction.js +33 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view.d.ts +32 -0
- package/dist/view.js +8 -0
- package/dist/workflow.d.ts +18 -1
- package/dist/workflow.js +39 -0
- package/package.json +3 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// A keyed view: several entities' changes, each through a handler of its own, written to whichever rows
|
|
2
|
+
// a change is about, by key. The sidecar runs one projection per source and handles one change at a
|
|
3
|
+
// time across all of them; this process only says which rows each change writes.
|
|
4
|
+
//
|
|
5
|
+
// export class Shipments extends KeyedView<ShipmentRow> {
|
|
6
|
+
// static readonly componentId = "shipments"
|
|
7
|
+
// static readonly row = ShipmentRowShape
|
|
8
|
+
// static readonly sources = [
|
|
9
|
+
// on(ShipmentEntity, ShipmentEvents, (v: Shipments, e) => v.onShipment(e)),
|
|
10
|
+
// on(CustomerEntity, CustomerEvents, (v: Shipments, e) => v.onCustomer(e)),
|
|
11
|
+
// ]
|
|
12
|
+
// static readonly declared = [declaredQuery("of-customer", `SELECT payload FROM ${tableOf("shipments")} WHERE ...`)]
|
|
13
|
+
// async onCustomer(event: CustomerEvent) {
|
|
14
|
+
// const theirs = await this.rows.ask("of-customer", { customer: this.subject })
|
|
15
|
+
// return this.effects.updateRows(theirs.map((r) => [r.shipmentId, { ...r, name: event.name }]))
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
18
|
+
import { KeyedViewEffects } from "./effects/keyed.js";
|
|
19
|
+
/**
|
|
20
|
+
* Declares a source of a keyed view: the entity whose changes it reads, the shape of those changes, and
|
|
21
|
+
* the handler for each. When the entity is deleted the view does nothing unless `deleted` names rows.
|
|
22
|
+
*/
|
|
23
|
+
export function on(source, events, onChange, options = {}) {
|
|
24
|
+
return Object.freeze({ source, events, onChange, ...(options.deleted ? { deleted: options.deleted } : {}) });
|
|
25
|
+
}
|
|
26
|
+
export class KeyedView {
|
|
27
|
+
effects = new KeyedViewEffects();
|
|
28
|
+
#metadata = {};
|
|
29
|
+
#client;
|
|
30
|
+
#rows;
|
|
31
|
+
/** The change's metadata: `ce-subject` is the source entity's id, `ankka.sequence` its sequence number. */
|
|
32
|
+
get metadata() {
|
|
33
|
+
return this.#metadata;
|
|
34
|
+
}
|
|
35
|
+
/** The id of the entity the change came from. */
|
|
36
|
+
get subject() {
|
|
37
|
+
return this.#metadata["ce-subject"] ?? "";
|
|
38
|
+
}
|
|
39
|
+
/** This view's own rows, by key or by one of its declared queries. */
|
|
40
|
+
get rows() {
|
|
41
|
+
if (!this.#rows)
|
|
42
|
+
throw new Error("rows are only available inside a keyed view's handler");
|
|
43
|
+
return this.#rows;
|
|
44
|
+
}
|
|
45
|
+
get client() {
|
|
46
|
+
if (!this.#client)
|
|
47
|
+
throw new Error("client is only available inside a keyed view's handler");
|
|
48
|
+
return this.#client;
|
|
49
|
+
}
|
|
50
|
+
/** @internal */
|
|
51
|
+
get _kind() {
|
|
52
|
+
return "view";
|
|
53
|
+
}
|
|
54
|
+
/** @internal */
|
|
55
|
+
_bind(metadata, client, rows) {
|
|
56
|
+
this.#metadata = metadata;
|
|
57
|
+
this.#client = client;
|
|
58
|
+
this.#rows = rows;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The rows a keyed view reads through the component client, on its own id. */
|
|
62
|
+
export function clientRows(client, viewId, row) {
|
|
63
|
+
return {
|
|
64
|
+
get: (key) => client.views.get(viewId, key, row),
|
|
65
|
+
ask: (name, values = {}) => client.views.ask(viewId, name, values, row),
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Approval } from "./approvals.ts";
|
|
2
|
+
export declare const VARIABLE_PREFIX = "ANKKA_MCP_";
|
|
3
|
+
export declare const TOOL_PREFIX = "mcp__";
|
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
5
|
+
export interface McpServerOptions {
|
|
6
|
+
/** Where the server is; `ANKKA_MCP_<NAME>_URL` replaces it, and is required when neither this nor `service` is given. */
|
|
7
|
+
readonly url?: string;
|
|
8
|
+
/** An ankka service, called as one, at `path` (by default `/mcp`), in `project` (by default this one). */
|
|
9
|
+
readonly service?: string;
|
|
10
|
+
readonly project?: string;
|
|
11
|
+
readonly path?: string;
|
|
12
|
+
/** A header's name and the `ANKKA_MCP_` variable its value is read from. */
|
|
13
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
14
|
+
/** Every tool of the server waits for a person. */
|
|
15
|
+
readonly approval?: Approval;
|
|
16
|
+
}
|
|
17
|
+
export interface McpServerRef extends McpServerOptions {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
}
|
|
20
|
+
/** An MCP server the agent lists under `name`, lower-case letters, digits and hyphens. */
|
|
21
|
+
export declare function mcpServer(name: string, options?: McpServerOptions): McpServerRef;
|
|
22
|
+
/** Checks what an MCP server's tool answered before the model is told it: a reason to withhold it, or `null`. */
|
|
23
|
+
export interface ResultGuardrailRef {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly check: (tool: string, text: string) => MaybePromise<string | null>;
|
|
26
|
+
}
|
|
27
|
+
export declare function resultGuardrail(name: string, check: (tool: string, text: string) => MaybePromise<string | null>): ResultGuardrailRef;
|
|
28
|
+
/** The name a server's tool is offered to the model under. */
|
|
29
|
+
export declare function toolName(server: string, tool: string): string;
|
|
30
|
+
/** What is wrong with one server, in the sidecar's words. */
|
|
31
|
+
export declare function serverProblems(server: McpServerRef): string[];
|
|
32
|
+
/** The discovery form of a server. */
|
|
33
|
+
export declare function serverToProto(server: McpServerRef): {
|
|
34
|
+
name: string;
|
|
35
|
+
address: {
|
|
36
|
+
case: "url";
|
|
37
|
+
value: string;
|
|
38
|
+
} | {
|
|
39
|
+
case: "service";
|
|
40
|
+
value: {
|
|
41
|
+
name: string;
|
|
42
|
+
path: string;
|
|
43
|
+
project?: string | undefined;
|
|
44
|
+
};
|
|
45
|
+
} | {
|
|
46
|
+
value?: undefined;
|
|
47
|
+
case: undefined;
|
|
48
|
+
};
|
|
49
|
+
headers: {
|
|
50
|
+
name: string;
|
|
51
|
+
variable: string;
|
|
52
|
+
}[];
|
|
53
|
+
approval?: {
|
|
54
|
+
withinMillis?: bigint;
|
|
55
|
+
} | undefined;
|
|
56
|
+
};
|
|
57
|
+
export {};
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// MCP servers an agent offers the model, and the guardrails that check what they answer.
|
|
2
|
+
//
|
|
3
|
+
// The sidecar connects to each server when it starts, reads its tools and offers them to the model as
|
|
4
|
+
// `mcp__<server>__<tool>`. This process is never asked to run one, and never sees a credential: a
|
|
5
|
+
// header's value is read from a variable on the sidecar, and only `ANKKA_MCP_` variables reach it. What a
|
|
6
|
+
// server's tool answers is put through the agent's result guardrails, which this process answers,
|
|
7
|
+
// before the model is told it.
|
|
8
|
+
//
|
|
9
|
+
// static readonly mcpServers = { tickets: mcpServer("tickets", { headers: { Authorization: "ANKKA_MCP_TICKETS_TOKEN" } }) }
|
|
10
|
+
// static readonly resultGuardrails = { noInstructions: resultGuardrail("no-instructions", (tool, text) => null) }
|
|
11
|
+
import { approvalProblem, approvalToProto } from "./approvals.js";
|
|
12
|
+
export const VARIABLE_PREFIX = "ANKKA_MCP_";
|
|
13
|
+
export const TOOL_PREFIX = "mcp__";
|
|
14
|
+
const NAME = /^[a-z0-9-]+$/;
|
|
15
|
+
/** An MCP server the agent lists under `name`, lower-case letters, digits and hyphens. */
|
|
16
|
+
export function mcpServer(name, options = {}) {
|
|
17
|
+
return Object.freeze({ ...options, name });
|
|
18
|
+
}
|
|
19
|
+
export function resultGuardrail(name, check) {
|
|
20
|
+
if (typeof name !== "string" || name.trim() === "")
|
|
21
|
+
throw new TypeError("a result guardrail needs a name");
|
|
22
|
+
return Object.freeze({ name, check });
|
|
23
|
+
}
|
|
24
|
+
/** The name a server's tool is offered to the model under. */
|
|
25
|
+
export function toolName(server, tool) {
|
|
26
|
+
return `${TOOL_PREFIX}${server}__${tool}`;
|
|
27
|
+
}
|
|
28
|
+
/** What is wrong with one server, in the sidecar's words. */
|
|
29
|
+
export function serverProblems(server) {
|
|
30
|
+
const found = [];
|
|
31
|
+
const name = server.name;
|
|
32
|
+
if (!NAME.test(name))
|
|
33
|
+
found.push(`an MCP server's name is lower-case letters, digits and hyphens ([a-z0-9-]), not '${name}'`);
|
|
34
|
+
if (server.url !== undefined && server.service !== undefined)
|
|
35
|
+
found.push(`MCP server '${name}' has both a URL and a service; give one`);
|
|
36
|
+
if (server.url !== undefined && server.url.trim() === "")
|
|
37
|
+
found.push(`MCP server '${name}' needs a URL`);
|
|
38
|
+
for (const [header, variable] of Object.entries(server.headers ?? {})) {
|
|
39
|
+
if (header.trim() === "")
|
|
40
|
+
found.push(`MCP server '${name}' has a header with no name`);
|
|
41
|
+
else if (!variable.startsWith(VARIABLE_PREFIX)) {
|
|
42
|
+
found.push(`MCP server '${name}': the header '${header}' takes its value from '${variable}', which must start ${VARIABLE_PREFIX} — only those variables reach the platform's program, which is what connects to the server`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const approval = approvalProblem(server.approval);
|
|
46
|
+
if (approval)
|
|
47
|
+
found.push(`MCP server '${name}': ${approval}`);
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
/** The discovery form of a server. */
|
|
51
|
+
export function serverToProto(server) {
|
|
52
|
+
const approval = approvalToProto(server.approval);
|
|
53
|
+
const address = server.url !== undefined
|
|
54
|
+
? { case: "url", value: server.url }
|
|
55
|
+
: server.service !== undefined
|
|
56
|
+
? { case: "service", value: { name: server.service, path: server.path ?? "/mcp", ...(server.project !== undefined ? { project: server.project } : {}) } }
|
|
57
|
+
: { case: undefined };
|
|
58
|
+
return {
|
|
59
|
+
name: server.name,
|
|
60
|
+
address,
|
|
61
|
+
headers: Object.entries(server.headers ?? {}).map(([name, variable]) => ({ name, variable })),
|
|
62
|
+
...(approval !== undefined ? { approval } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Socket } from "./socket.ts";
|
|
1
2
|
import type { Shape } from "./codec.ts";
|
|
2
3
|
import type { RequestContext } from "./context.ts";
|
|
3
4
|
import type { Infer, Schema } from "./schema.ts";
|
|
@@ -74,8 +75,10 @@ export interface RouteRef<Ep = unknown, P = unknown, B = unknown, R = unknown> {
|
|
|
74
75
|
readonly body: Shape<B> | undefined;
|
|
75
76
|
readonly reply: Shape<R> | undefined;
|
|
76
77
|
readonly streaming: boolean;
|
|
78
|
+
/** Answered by opening a socket (protocol 1.9); `run`'s third argument is then the `Socket`. */
|
|
79
|
+
readonly socket: boolean;
|
|
77
80
|
readonly acl: Acl | undefined;
|
|
78
|
-
readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string>;
|
|
81
|
+
readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string | SseEvent>;
|
|
79
82
|
}
|
|
80
83
|
export declare function parseTemplate(template: string): string[];
|
|
81
84
|
type NoBodyRun<Ep, T extends string, PS, R> = (self: Ep, request: RequestContext<Params<T, PS>>) => MaybePromise<R>;
|
|
@@ -100,8 +103,25 @@ export declare const del: {
|
|
|
100
103
|
<Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
|
|
101
104
|
<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
102
105
|
};
|
|
103
|
-
/**
|
|
104
|
-
|
|
106
|
+
/**
|
|
107
|
+
* A server-sent event under a name of its own, which an `sse` handler may yield beside text: an agent
|
|
108
|
+
* turn that stops for approval ends its stream with `sseEvent("approval", requests)`. `value` is sent as
|
|
109
|
+
* JSON. Needs protocol 1.11 of the runtime.
|
|
110
|
+
*/
|
|
111
|
+
export interface SseEvent {
|
|
112
|
+
readonly kind: "sse-event";
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly value: unknown;
|
|
115
|
+
}
|
|
116
|
+
export declare function sseEvent(name: string, value: unknown): SseEvent;
|
|
117
|
+
/** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
|
|
118
|
+
export declare function sse<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>) => AsyncIterable<string | SseEvent> | Promise<AsyncIterable<string | SseEvent>>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, never>;
|
|
119
|
+
/**
|
|
120
|
+
* A socket route: `socket("/{room}", async (ep: Rooms, req, socket) => { for await (const t of socket) await socket.send(t) })`.
|
|
121
|
+
* The handler runs for as long as the socket is open, with the opening request as `req` throughout.
|
|
122
|
+
* Needs protocol 1.9 of the runtime.
|
|
123
|
+
*/
|
|
124
|
+
export declare function socket<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>, socket: Socket) => Promise<void>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, Socket, void>;
|
|
105
125
|
/** The routes table type an endpoint declares. */
|
|
106
126
|
export type RouteTable<Ep> = Readonly<Record<string, RouteRef<Ep, any, any, any>>>;
|
|
107
127
|
export {};
|
package/dist/routes.js
CHANGED
|
@@ -57,7 +57,7 @@ export function parseTemplate(template) {
|
|
|
57
57
|
throw new TypeError(`route template ${template} repeats a parameter`);
|
|
58
58
|
return names;
|
|
59
59
|
}
|
|
60
|
-
function route(method, template, body, reply, run, options, streaming) {
|
|
60
|
+
function route(method, template, body, reply, run, options, streaming, socket = false) {
|
|
61
61
|
const paramNames = parseTemplate(template);
|
|
62
62
|
if (typeof run !== "function")
|
|
63
63
|
throw new TypeError(`route ${method} ${template}: the handler is not a function`);
|
|
@@ -68,7 +68,7 @@ function route(method, template, body, reply, run, options, streaming) {
|
|
|
68
68
|
if (!paramNames.includes(name))
|
|
69
69
|
throw new TypeError(`route ${method} ${template}: params names ${name}, which is not in the template`);
|
|
70
70
|
}
|
|
71
|
-
return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, acl: options?.acl, run });
|
|
71
|
+
return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, socket, acl: options?.acl, run });
|
|
72
72
|
}
|
|
73
73
|
function isShape(x) {
|
|
74
74
|
return typeof x === "object" && x !== null && (typeof x.kind === "string" || isCodec(x));
|
|
@@ -96,7 +96,20 @@ export const put = withBody("PUT");
|
|
|
96
96
|
export const patch = withBody("PATCH");
|
|
97
97
|
/** A DELETE route; `delete` is a reserved word, so `del`. */
|
|
98
98
|
export const del = withBody("DELETE");
|
|
99
|
-
|
|
99
|
+
export function sseEvent(name, value) {
|
|
100
|
+
if (typeof name !== "string" || name === "" || /[\r\n]/.test(name))
|
|
101
|
+
throw new TypeError(`an event name must be one non-empty line, not ${JSON.stringify(name)}`);
|
|
102
|
+
return Object.freeze({ kind: "sse-event", name, value });
|
|
103
|
+
}
|
|
104
|
+
/** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
|
|
100
105
|
export function sse(template, run, options) {
|
|
101
106
|
return route("GET", template, undefined, undefined, run, options, true);
|
|
102
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* A socket route: `socket("/{room}", async (ep: Rooms, req, socket) => { for await (const t of socket) await socket.send(t) })`.
|
|
110
|
+
* The handler runs for as long as the socket is open, with the opening request as `req` throughout.
|
|
111
|
+
* Needs protocol 1.9 of the runtime.
|
|
112
|
+
*/
|
|
113
|
+
export function socket(template, run, options) {
|
|
114
|
+
return route("GET", template, undefined, undefined, run, options, false, true);
|
|
115
|
+
}
|
package/dist/server/agent.js
CHANGED
|
@@ -84,7 +84,10 @@ export async function handleTool(req, ctx) {
|
|
|
84
84
|
if (!registered)
|
|
85
85
|
return create(ToolResultSchema, { result: { case: "error", value: `no agent ${JSON.stringify(req.componentId)} is registered` } });
|
|
86
86
|
const agent = new registered.cls();
|
|
87
|
-
|
|
87
|
+
// What the sidecar says about the tool's run: its trace, and the agent's handler as the caller of
|
|
88
|
+
// whatever the tool calls. A sidecar before 1.3 sends none.
|
|
89
|
+
const metadata = metadataFromProto(req.metadata);
|
|
90
|
+
agent._bind(req.sessionId, metadata, ctx.client.withMetadata(metadata));
|
|
88
91
|
try {
|
|
89
92
|
return create(ToolResultSchema, { result: { case: "ok", value: await runTool(registered, agent, req.tool, req.argumentsJson) } });
|
|
90
93
|
}
|
|
@@ -95,11 +98,16 @@ export async function handleTool(req, ctx) {
|
|
|
95
98
|
}
|
|
96
99
|
export async function handleGuardrail(req, ctx) {
|
|
97
100
|
const registered = ctx.registry.of("agent", req.componentId) ?? ctx.registry.of("autonomous-agent", req.componentId);
|
|
98
|
-
|
|
101
|
+
// RESULT (1.11): what an MCP server's tool answered, checked by one of the agent's result guardrails,
|
|
102
|
+
// which are declared apart from its other guardrails.
|
|
103
|
+
const result = req.stage === GuardrailRequest_Stage.RESULT;
|
|
104
|
+
const guardrail = result ? registered?.resultGuardrails.get(req.guardrail) : registered?.guardrails.get(req.guardrail);
|
|
99
105
|
if (!registered || !guardrail)
|
|
100
106
|
return create(GuardrailResultSchema, { result: { case: "block", value: `no guardrail ${JSON.stringify(req.guardrail)} on ${req.componentId}` } });
|
|
101
107
|
try {
|
|
102
|
-
const reason =
|
|
108
|
+
const reason = result
|
|
109
|
+
? await guardrail.check(req.tool ?? "", req.text)
|
|
110
|
+
: await guardrail.check(req.stage === GuardrailRequest_Stage.OUTPUT ? "output" : "input", req.text);
|
|
103
111
|
return reason === null || reason === undefined
|
|
104
112
|
? create(GuardrailResultSchema, { result: { case: "pass", value: {} } })
|
|
105
113
|
: create(GuardrailResultSchema, { result: { case: "block", value: reason } });
|
|
@@ -114,7 +122,7 @@ async function handleAutonomousTool(registered, req, ctx) {
|
|
|
114
122
|
if (!tool)
|
|
115
123
|
return create(ToolResultSchema, { result: { case: "error", value: `no tool ${JSON.stringify(req.tool)} on ${registered.id}` } });
|
|
116
124
|
const agent = new registered.cls();
|
|
117
|
-
agent._bind(req.sessionId, ctx.client);
|
|
125
|
+
agent._bind(req.sessionId, ctx.client.withMetadata(metadataFromProto(req.metadata)));
|
|
118
126
|
try {
|
|
119
127
|
let input;
|
|
120
128
|
if (isCodec(tool.input))
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ConnectRouter } from "@connectrpc/connect";
|
|
2
2
|
import { type Spec } from "../_proto/ankka/protocol/v1/discovery_pb.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A sidecar older than 1.7 would ignore where a topic source starts and its version: a consumer
|
|
5
|
+
* declared `latest` would read everything, and a raised version would rebuild nothing. Refused,
|
|
6
|
+
* naming what declares them, rather than served wrong.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refusal(spec: Spec, sidecarProtocol: string): string | undefined;
|
|
3
9
|
/** Every problem the sidecar has reported to this process, in order. */
|
|
4
10
|
export declare const problems: string[];
|
|
5
11
|
export declare function discoveryRoutes(router: ConnectRouter, spec: () => Spec, log?: (message: string) => void): void;
|
|
12
|
+
/** The protocol version whose runtimes serve socket routes. */
|
|
13
|
+
export declare const SOCKETS_SINCE: readonly [number, number];
|
|
14
|
+
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.9
|
|
15
|
+
* does not know a socket route, and would serve it as a plain GET. */
|
|
16
|
+
export declare function socketRefusal(spec: Spec, runtime: string): string | undefined;
|
package/dist/server/discovery.js
CHANGED
|
@@ -1,16 +1,96 @@
|
|
|
1
1
|
// Discovery: the first conversation. The sidecar asks what we host; we answer with the Spec the
|
|
2
2
|
// registry renders. `ReportError` is how the sidecar tells us why it refused to start — logged, and kept
|
|
3
3
|
// so a service can expose it (the conformance reference does, at GET /conformance/problems).
|
|
4
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
4
5
|
import { create } from "@bufbuild/protobuf";
|
|
5
6
|
import { Discovery } from "../_proto/ankka/protocol/v1/discovery_pb.js";
|
|
6
7
|
import { EmptySchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
|
|
8
|
+
import { PROTOCOL_VERSION } from "../spec.js";
|
|
9
|
+
import { olderThanContracts, olderThanDeclaredQueries, olderThanStartPositions } from "../startFrom.js";
|
|
10
|
+
/**
|
|
11
|
+
* A sidecar older than 1.7 would ignore where a topic source starts and its version: a consumer
|
|
12
|
+
* declared `latest` would read everything, and a raised version would rebuild nothing. Refused,
|
|
13
|
+
* naming what declares them, rather than served wrong.
|
|
14
|
+
*/
|
|
15
|
+
export function refusal(spec, sidecarProtocol) {
|
|
16
|
+
return startPositionRefusal(spec, sidecarProtocol) ?? declaredQueryRefusal(spec, sidecarProtocol) ?? contractRefusal(spec, sidecarProtocol);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A sidecar older than 1.14 would ignore a topic's contract, broker and parallel reading and a
|
|
20
|
+
* publication's contract and broker: a contract would go unchecked, a topic on another broker would be
|
|
21
|
+
* read from the installation's. Refused at discovery, naming the components, rather than served wrong.
|
|
22
|
+
*/
|
|
23
|
+
function contractRefusal(spec, sidecarProtocol) {
|
|
24
|
+
if (!olderThanContracts(sidecarProtocol))
|
|
25
|
+
return undefined;
|
|
26
|
+
const declaring = spec.components
|
|
27
|
+
.filter((c) => {
|
|
28
|
+
const d = c.detail;
|
|
29
|
+
if (d.case !== "view" && d.case !== "consumer")
|
|
30
|
+
return false;
|
|
31
|
+
const src = d.value.source;
|
|
32
|
+
const onSource = src !== undefined && (src.contract !== undefined || src.broker !== undefined || src.parallel === true);
|
|
33
|
+
const onPublication = d.case === "consumer" && d.value.produces !== undefined && (d.value.produces.contract !== undefined || d.value.produces.broker !== undefined);
|
|
34
|
+
return onSource || onPublication;
|
|
35
|
+
})
|
|
36
|
+
.map((c) => c.id);
|
|
37
|
+
if (declaring.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
return (`${declaring.join(", ")} declare a topic's contract, broker or parallel reading, which the sidecar ignores: ` +
|
|
40
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.14 or later.`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A sidecar older than 1.13 would ignore a view's declared queries, would read a keyed view as having
|
|
44
|
+
* no source, and would refuse a version on a view that reads an entity as applying only to a topic.
|
|
45
|
+
* Refused at discovery, naming the views, rather than found later or misreported.
|
|
46
|
+
*/
|
|
47
|
+
function declaredQueryRefusal(spec, sidecarProtocol) {
|
|
48
|
+
if (!olderThanDeclaredQueries(sidecarProtocol))
|
|
49
|
+
return undefined;
|
|
50
|
+
const declaring = spec.components
|
|
51
|
+
.filter((c) => {
|
|
52
|
+
if (c.detail.case !== "view")
|
|
53
|
+
return false;
|
|
54
|
+
const v = c.detail.value;
|
|
55
|
+
// A declared query, a keyed view, or a version on a view that reads an entity.
|
|
56
|
+
return v.declaredQueries.length > 0 || v.sources.length > 0 || (v.version !== undefined && v.source?.source.case === "component");
|
|
57
|
+
})
|
|
58
|
+
.map((c) => c.id);
|
|
59
|
+
if (declaring.length === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
return (`${declaring.join(", ")} declare queries, several sources, or a version on a view that reads an entity, which the sidecar ignores: ` +
|
|
62
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.13 or later.`);
|
|
63
|
+
}
|
|
64
|
+
function startPositionRefusal(spec, sidecarProtocol) {
|
|
65
|
+
if (!olderThanStartPositions(sidecarProtocol))
|
|
66
|
+
return undefined;
|
|
67
|
+
const declaring = spec.components
|
|
68
|
+
.filter((c) => {
|
|
69
|
+
const d = c.detail;
|
|
70
|
+
if (d.case !== "view" && d.case !== "consumer")
|
|
71
|
+
return false;
|
|
72
|
+
return d.value.source?.startFrom !== undefined || d.value.version !== undefined;
|
|
73
|
+
})
|
|
74
|
+
.map((c) => c.id);
|
|
75
|
+
if (declaring.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
return (`${declaring.join(", ")} declare where a topic source starts or its version, which the sidecar ignores: ` +
|
|
78
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.7 or later.`);
|
|
79
|
+
}
|
|
7
80
|
/** Every problem the sidecar has reported to this process, in order. */
|
|
8
81
|
export const problems = [];
|
|
9
82
|
export function discoveryRoutes(router, spec, log = console.error) {
|
|
10
83
|
router.service(Discovery, {
|
|
11
84
|
async discover(info) {
|
|
12
85
|
log(`ankka: discovery from sidecar (protocol ${info.protocolVersion}, runtime ${info.runtimeVersion})`);
|
|
13
|
-
|
|
86
|
+
const answer = spec();
|
|
87
|
+
const why = refusal(answer, info.protocolVersion) ?? socketRefusal(answer, info.protocolVersion);
|
|
88
|
+
if (why !== undefined) {
|
|
89
|
+
log(`ankka: ${why}`);
|
|
90
|
+
problems.push(why);
|
|
91
|
+
throw new ConnectError(why, Code.FailedPrecondition);
|
|
92
|
+
}
|
|
93
|
+
return answer;
|
|
14
94
|
},
|
|
15
95
|
async reportError(problem) {
|
|
16
96
|
log(`ankka: the sidecar refused to start: ${problem.message}`);
|
|
@@ -19,3 +99,17 @@ export function discoveryRoutes(router, spec, log = console.error) {
|
|
|
19
99
|
},
|
|
20
100
|
});
|
|
21
101
|
}
|
|
102
|
+
/** The protocol version whose runtimes serve socket routes. */
|
|
103
|
+
export const SOCKETS_SINCE = [1, 9];
|
|
104
|
+
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.9
|
|
105
|
+
* does not know a socket route, and would serve it as a plain GET. */
|
|
106
|
+
export function socketRefusal(spec, runtime) {
|
|
107
|
+
const routes = spec.endpoints.flatMap((e) => e.routes.filter((r) => r.socket).map((r) => `${e.id}.${r.id}`));
|
|
108
|
+
if (routes.length === 0)
|
|
109
|
+
return undefined;
|
|
110
|
+
const [major, minor] = runtime.split(".").map((part) => Number.parseInt(part, 10));
|
|
111
|
+
const speaks = Number.isFinite(major) && Number.isFinite(minor) ? [major, minor] : [0, 0];
|
|
112
|
+
if (speaks[0] > SOCKETS_SINCE[0] || (speaks[0] === SOCKETS_SINCE[0] && speaks[1] >= SOCKETS_SINCE[1]))
|
|
113
|
+
return undefined;
|
|
114
|
+
return `this runtime speaks protocol ${runtime || "unknown"}; a socket route needs 1.9 (${routes.join(", ")})`;
|
|
115
|
+
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
|
-
import { type HttpRequest, type HttpReply, type StreamFrame } from "../_proto/ankka/protocol/v1/endpoint_pb.ts";
|
|
2
|
+
import { type HttpRequest, type HttpReply, type SocketIn, type SocketOut, type StreamFrame } from "../_proto/ankka/protocol/v1/endpoint_pb.ts";
|
|
3
3
|
import type { ServerContext } from "./server.ts";
|
|
4
4
|
export interface HttpDispatcher {
|
|
5
5
|
handle(req: HttpRequest): Promise<HttpReply>;
|
|
6
6
|
handleStream(req: HttpRequest): AsyncIterable<StreamFrame>;
|
|
7
|
+
handleSocket(requests: AsyncIterable<SocketIn>): AsyncIterable<SocketOut>;
|
|
7
8
|
}
|
|
8
9
|
/** The behaviour behind `Http.Handle` and `Http.HandleStream`, over the registry's endpoints. */
|
|
9
10
|
export declare function createHttpDispatcher(ctx: ServerContext): HttpDispatcher;
|
package/dist/server/http.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// `createHttpDispatcher` is the whole behaviour; the Connect servicer and the endpoint unit testkit both
|
|
6
6
|
// call it, so a test cannot pass on a path the sidecar would not take.
|
|
7
7
|
import { create } from "@bufbuild/protobuf";
|
|
8
|
-
import { Http, HttpReplySchema, StreamFrameSchema } from "../_proto/ankka/protocol/v1/endpoint_pb.js";
|
|
8
|
+
import { Http, HttpReplySchema, SocketOutSchema, StreamFrameSchema, } from "../_proto/ankka/protocol/v1/endpoint_pb.js";
|
|
9
9
|
import { codecFor } from "../codec.js";
|
|
10
10
|
import { Headers, Query, metadataFromProto, withRequest } from "../context.js";
|
|
11
11
|
import { CommandError, ErrorCode, httpStatusOf } from "../effects/common.js";
|
|
@@ -13,6 +13,7 @@ import { DecodingError } from "../json.js";
|
|
|
13
13
|
import { errorCodeToProto } from "../kinds.js";
|
|
14
14
|
import { HttpProblem } from "../routes.js";
|
|
15
15
|
import { done, resolve } from "../schema.js";
|
|
16
|
+
import { SocketClosed, socketOf } from "../socket.js";
|
|
16
17
|
import { AsyncQueue } from "./queue.js";
|
|
17
18
|
const utf8 = new TextEncoder();
|
|
18
19
|
function textResponse(status, text) {
|
|
@@ -28,7 +29,15 @@ function principalOf(req) {
|
|
|
28
29
|
const p = req.principal;
|
|
29
30
|
if (!p)
|
|
30
31
|
return null;
|
|
31
|
-
return Object.freeze({
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
subject: p.subject,
|
|
34
|
+
name: p.name ?? null,
|
|
35
|
+
email: p.email ?? null,
|
|
36
|
+
emailVerified: p.emailVerified,
|
|
37
|
+
roles: Object.freeze([...p.roles]),
|
|
38
|
+
claims: Object.freeze({ ...p.claims }),
|
|
39
|
+
issuer: p.issuer ?? null,
|
|
40
|
+
});
|
|
32
41
|
}
|
|
33
42
|
/** The caller the sidecar established; a sidecar that predates protocol 1.1 sends none, which reads as local. */
|
|
34
43
|
function callerOf(req) {
|
|
@@ -69,6 +78,7 @@ function encodeResult(route, result) {
|
|
|
69
78
|
const contentType = codec.contentType === "text/plain" ? "text/plain; charset=utf-8" : codec.contentType;
|
|
70
79
|
return create(HttpReplySchema, { message: { case: "response", value: { status: 200, contentType, body: codec.encode(result), headers: [] } } });
|
|
71
80
|
}
|
|
81
|
+
const NEEDS_SOCKETS = "a socket route needs protocol 1.9; this runtime asked for a request";
|
|
72
82
|
/** The behaviour behind `Http.Handle` and `Http.HandleStream`, over the registry's endpoints. */
|
|
73
83
|
export function createHttpDispatcher(ctx) {
|
|
74
84
|
const instances = new Map();
|
|
@@ -118,6 +128,8 @@ export function createHttpDispatcher(ctx) {
|
|
|
118
128
|
const prep = prepare(req);
|
|
119
129
|
if (!prep.ok)
|
|
120
130
|
return prep.reply;
|
|
131
|
+
if (prep.prepared.route.socket)
|
|
132
|
+
return failureReply(NEEDS_SOCKETS);
|
|
121
133
|
const { route, instance, request, body } = prep.prepared;
|
|
122
134
|
try {
|
|
123
135
|
const result = await withRequest(request, () => route.run(instance, request, body));
|
|
@@ -135,13 +147,21 @@ export function createHttpDispatcher(ctx) {
|
|
|
135
147
|
return;
|
|
136
148
|
}
|
|
137
149
|
const { route, instance, request } = prep.prepared;
|
|
150
|
+
if (route.socket) {
|
|
151
|
+
yield create(StreamFrameSchema, { frame: { case: "failed", value: { message: NEEDS_SOCKETS, code: errorCodeToProto(ErrorCode.Internal) } } });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
138
154
|
// Produce inside the request's async context, consume here: the handler's `this.request` stays visible.
|
|
139
155
|
const queue = new AsyncQueue();
|
|
140
156
|
void withRequest(request, async () => {
|
|
141
157
|
try {
|
|
142
158
|
const frames = (await route.run(instance, request, undefined));
|
|
143
|
-
for await (const
|
|
144
|
-
|
|
159
|
+
for await (const part of frames) {
|
|
160
|
+
if (typeof part === "string")
|
|
161
|
+
queue.push(create(StreamFrameSchema, { frame: { case: "text", value: part } }));
|
|
162
|
+
else
|
|
163
|
+
queue.push(create(StreamFrameSchema, { frame: { case: "event", value: { name: part.name, data: JSON.stringify(part.value, (_k, v) => (typeof v === "bigint" ? v.toString() : v)) } } }));
|
|
164
|
+
}
|
|
145
165
|
queue.push(create(StreamFrameSchema, { frame: { case: "completed", value: {} } }));
|
|
146
166
|
}
|
|
147
167
|
catch (e) {
|
|
@@ -155,6 +175,59 @@ export function createHttpDispatcher(ctx) {
|
|
|
155
175
|
});
|
|
156
176
|
yield* queue;
|
|
157
177
|
},
|
|
178
|
+
// An open socket: `open` first, then frames until `closed`. The requests are read only when the
|
|
179
|
+
// handler asks for a frame, so a handler that does not read is not read for, and the sidecar's flow
|
|
180
|
+
// control holds the client back.
|
|
181
|
+
async *handleSocket(requests) {
|
|
182
|
+
const incoming = requests[Symbol.asyncIterator]();
|
|
183
|
+
const failed = (message, code = ErrorCode.Internal) => create(SocketOutSchema, { message: { case: "failed", value: { message, code: errorCodeToProto(code) } } });
|
|
184
|
+
const first = await incoming.next();
|
|
185
|
+
if (first.done || first.value.message.case !== "open") {
|
|
186
|
+
yield failed("a socket opens with open");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const prep = prepare(first.value.message.value);
|
|
190
|
+
if (!prep.ok || !prep.prepared.route.socket) {
|
|
191
|
+
yield failed("unknown socket route", ErrorCode.NotFound);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const { route, instance, request } = prep.prepared;
|
|
195
|
+
const outgoing = new AsyncQueue();
|
|
196
|
+
let closed = false;
|
|
197
|
+
const receive = async () => {
|
|
198
|
+
if (closed)
|
|
199
|
+
return undefined;
|
|
200
|
+
const next = await incoming.next();
|
|
201
|
+
const message = next.done ? undefined : next.value.message;
|
|
202
|
+
if (message?.case === "frame" && message.value.kind.case === "text")
|
|
203
|
+
return message.value.kind.value;
|
|
204
|
+
closed = true; // closed, the stream's end, or a kind this SDK does not know
|
|
205
|
+
return undefined;
|
|
206
|
+
};
|
|
207
|
+
const send = async (text) => {
|
|
208
|
+
if (closed)
|
|
209
|
+
throw new SocketClosed();
|
|
210
|
+
outgoing.push(create(SocketOutSchema, { message: { case: "frame", value: { kind: { case: "text", value: text } } } }));
|
|
211
|
+
};
|
|
212
|
+
void withRequest(request, async () => {
|
|
213
|
+
try {
|
|
214
|
+
await route.run(instance, request, socketOf(receive, send));
|
|
215
|
+
outgoing.push(create(SocketOutSchema, { message: { case: "completed", value: {} } }));
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
if (e instanceof SocketClosed)
|
|
219
|
+
outgoing.push(create(SocketOutSchema, { message: { case: "completed", value: {} } }));
|
|
220
|
+
else {
|
|
221
|
+
ctx.log(`ankka: SOCKET ${route.template} threw: ${messageOf(e)}`);
|
|
222
|
+
outgoing.push(failed(messageOf(e)));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
outgoing.close();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
yield* outgoing;
|
|
230
|
+
},
|
|
158
231
|
};
|
|
159
232
|
}
|
|
160
233
|
export function httpRoutes(router, ctx) {
|
|
@@ -162,5 +235,6 @@ export function httpRoutes(router, ctx) {
|
|
|
162
235
|
router.service(Http, {
|
|
163
236
|
handle: (req) => dispatcher.handle(req),
|
|
164
237
|
handleStream: (req) => dispatcher.handleStream(req),
|
|
238
|
+
handleSocket: (requests) => dispatcher.handleSocket(requests),
|
|
165
239
|
});
|
|
166
240
|
}
|