ankka 0.6.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.
Files changed (108) hide show
  1. package/README.md +30 -0
  2. package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +350 -0
  3. package/dist/_proto/ankka/protocol/v1/agent_pb.js +90 -0
  4. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +254 -0
  5. package/dist/_proto/ankka/protocol/v1/client_pb.js +47 -0
  6. package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +105 -0
  7. package/dist/_proto/ankka/protocol/v1/consumer_pb.js +28 -0
  8. package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +598 -0
  9. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +175 -0
  10. package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +224 -0
  11. package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +44 -0
  12. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +212 -0
  13. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +47 -0
  14. package/dist/_proto/ankka/protocol/v1/key_value_pb.d.ts +154 -0
  15. package/dist/_proto/ankka/protocol/v1/key_value_pb.js +36 -0
  16. package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +276 -0
  17. package/dist/_proto/ankka/protocol/v1/payload_pb.js +110 -0
  18. package/dist/_proto/ankka/protocol/v1/timed_action_pb.d.ts +75 -0
  19. package/dist/_proto/ankka/protocol/v1/timed_action_pb.js +23 -0
  20. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +93 -0
  21. package/dist/_proto/ankka/protocol/v1/view_pb.js +23 -0
  22. package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +298 -0
  23. package/dist/_proto/ankka/protocol/v1/workflow_pb.js +67 -0
  24. package/dist/agent.d.ts +27 -0
  25. package/dist/agent.js +36 -0
  26. package/dist/client.d.ts +92 -0
  27. package/dist/client.js +239 -0
  28. package/dist/codec.d.ts +44 -0
  29. package/dist/codec.js +215 -0
  30. package/dist/consumer.d.ts +31 -0
  31. package/dist/consumer.js +32 -0
  32. package/dist/context.d.ts +61 -0
  33. package/dist/context.js +75 -0
  34. package/dist/effects/agent.d.ts +37 -0
  35. package/dist/effects/agent.js +48 -0
  36. package/dist/effects/common.d.ts +68 -0
  37. package/dist/effects/common.js +55 -0
  38. package/dist/effects/eventSourced.d.ts +50 -0
  39. package/dist/effects/eventSourced.js +66 -0
  40. package/dist/effects/keyValue.d.ts +36 -0
  41. package/dist/effects/keyValue.js +47 -0
  42. package/dist/effects/stateless.d.ts +40 -0
  43. package/dist/effects/stateless.js +35 -0
  44. package/dist/effects/workflow.d.ts +99 -0
  45. package/dist/effects/workflow.js +83 -0
  46. package/dist/endpoint.d.ts +21 -0
  47. package/dist/endpoint.js +29 -0
  48. package/dist/eventSourcedEntity.d.ts +40 -0
  49. package/dist/eventSourcedEntity.js +66 -0
  50. package/dist/handlers.d.ts +55 -0
  51. package/dist/handlers.js +64 -0
  52. package/dist/index.d.ts +29 -0
  53. package/dist/index.js +39 -0
  54. package/dist/json.d.ts +21 -0
  55. package/dist/json.js +362 -0
  56. package/dist/keyValueEntity.d.ts +29 -0
  57. package/dist/keyValueEntity.js +52 -0
  58. package/dist/kinds.d.ts +8 -0
  59. package/dist/kinds.js +41 -0
  60. package/dist/materialise.d.ts +33 -0
  61. package/dist/materialise.js +55 -0
  62. package/dist/routes.d.ts +71 -0
  63. package/dist/routes.js +79 -0
  64. package/dist/schema.d.ts +112 -0
  65. package/dist/schema.js +200 -0
  66. package/dist/server/agent.d.ts +14 -0
  67. package/dist/server/agent.js +113 -0
  68. package/dist/server/discovery.d.ts +5 -0
  69. package/dist/server/discovery.js +21 -0
  70. package/dist/server/eventSourced.d.ts +12 -0
  71. package/dist/server/eventSourced.js +168 -0
  72. package/dist/server/http.d.ts +10 -0
  73. package/dist/server/http.js +153 -0
  74. package/dist/server/keyValue.d.ts +5 -0
  75. package/dist/server/keyValue.js +122 -0
  76. package/dist/server/payloads.d.ts +6 -0
  77. package/dist/server/payloads.js +11 -0
  78. package/dist/server/queue.d.ts +10 -0
  79. package/dist/server/queue.js +67 -0
  80. package/dist/server/server.d.ts +34 -0
  81. package/dist/server/server.js +101 -0
  82. package/dist/server/stateless.d.ts +9 -0
  83. package/dist/server/stateless.js +108 -0
  84. package/dist/server/workflow.d.ts +5 -0
  85. package/dist/server/workflow.js +244 -0
  86. package/dist/service.d.ts +147 -0
  87. package/dist/service.js +384 -0
  88. package/dist/spec.d.ts +7 -0
  89. package/dist/spec.js +106 -0
  90. package/dist/testkit/index.d.ts +3 -0
  91. package/dist/testkit/index.js +6 -0
  92. package/dist/testkit/integration.d.ts +60 -0
  93. package/dist/testkit/integration.js +284 -0
  94. package/dist/testkit/kinds.d.ts +133 -0
  95. package/dist/testkit/kinds.js +398 -0
  96. package/dist/testkit/unit.d.ts +54 -0
  97. package/dist/testkit/unit.js +224 -0
  98. package/dist/time.d.ts +73 -0
  99. package/dist/time.js +275 -0
  100. package/dist/timedAction.d.ts +20 -0
  101. package/dist/timedAction.js +28 -0
  102. package/dist/version.d.ts +1 -0
  103. package/dist/version.js +2 -0
  104. package/dist/view.d.ts +37 -0
  105. package/dist/view.js +52 -0
  106. package/dist/workflow.d.ts +33 -0
  107. package/dist/workflow.js +61 -0
  108. package/package.json +75 -0
package/dist/routes.js ADDED
@@ -0,0 +1,79 @@
1
+ // Route declarations for an endpoint's static `routes` table, and the access rules. A route names its
2
+ // template, its body and reply shapes and the function that runs it; the sidecar serves the route,
3
+ // applies the access rule and forwards the request, and the process only runs the handler.
4
+ //
5
+ // static readonly routes = {
6
+ // addItem: post("/{cartId}/items", LineItem, Done, (ep: CartEndpoint, req, item) => ep.addItem(req.params.cartId, item)),
7
+ // getCart: get("/{cartId}", ShoppingCart, (ep: CartEndpoint, req) => ep.getCart(req.params.cartId)),
8
+ // }
9
+ //
10
+ // `req.params` is typed from the template: "/{cartId}/items" gives { cartId: string }.
11
+ import { isCodec } from "./codec.js";
12
+ /** Who may call: everyone, no one, or authenticated callers only. There is no default. */
13
+ export const Acl = Object.freeze({
14
+ allowAll: "allow-all",
15
+ denyAll: "deny-all",
16
+ authenticated: "authenticated",
17
+ });
18
+ /** Thrown from a route handler to answer a status other than 200 with a plain-text message. */
19
+ export class HttpProblem extends Error {
20
+ status;
21
+ constructor(status, message) {
22
+ super(message);
23
+ this.name = "HttpProblem";
24
+ this.status = status;
25
+ }
26
+ }
27
+ const TEMPLATE = /^(\/(?:[^/{}]+|\{[A-Za-z_][A-Za-z0-9_]*\})?)*$/;
28
+ export function parseTemplate(template) {
29
+ if (typeof template !== "string" || !template.startsWith("/") || !TEMPLATE.test(template) || template.includes("//")) {
30
+ throw new TypeError(`not a route template: ${JSON.stringify(template)} (segments are literals or {name})`);
31
+ }
32
+ const names = [...template.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
33
+ if (new Set(names).size !== names.length)
34
+ throw new TypeError(`route template ${template} repeats a parameter`);
35
+ return names;
36
+ }
37
+ function route(method, template, body, reply, run, options, streaming) {
38
+ const paramNames = parseTemplate(template);
39
+ if (typeof run !== "function")
40
+ throw new TypeError(`route ${method} ${template}: the handler is not a function`);
41
+ if (method === "GET" && body !== undefined)
42
+ throw new TypeError(`route GET ${template} cannot have a body`);
43
+ const paramShapes = options?.params ?? {};
44
+ for (const name of Object.keys(paramShapes)) {
45
+ if (!paramNames.includes(name))
46
+ throw new TypeError(`route ${method} ${template}: params names ${name}, which is not in the template`);
47
+ }
48
+ return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, acl: options?.acl, run });
49
+ }
50
+ function isShape(x) {
51
+ return typeof x === "object" && x !== null && (typeof x.kind === "string" || isCodec(x));
52
+ }
53
+ /** A GET route: `get("/{cartId}", ShoppingCart, (ep: CartEndpoint, req) => ep.getCart(req.params.cartId))`. */
54
+ export function get(template, reply, run, options) {
55
+ return route("GET", template, undefined, reply, run, options, false);
56
+ }
57
+ function withBody(method) {
58
+ function declare(template, ...rest) {
59
+ if (typeof rest[1] === "function") {
60
+ const [reply, run, options] = rest;
61
+ return route(method, template, undefined, reply, run, options, false);
62
+ }
63
+ const [body, reply, run, options] = rest;
64
+ if (!isShape(body))
65
+ throw new TypeError(`route ${method} ${template}: expected a body shape`);
66
+ return route(method, template, body, reply, run, options, false);
67
+ }
68
+ return declare;
69
+ }
70
+ /** A POST route, with a body (`post(template, Body, Reply, run)`) or without (`post(template, Reply, run)`). */
71
+ export const post = withBody("POST");
72
+ export const put = withBody("PUT");
73
+ export const patch = withBody("PATCH");
74
+ /** A DELETE route; `delete` is a reserved word, so `del`. */
75
+ export const del = withBody("DELETE");
76
+ /** A server-sent-events route: the handler returns an `AsyncIterable<string>`, one frame per string. */
77
+ export function sse(template, run, options) {
78
+ return route("GET", template, undefined, undefined, run, options, true);
79
+ }
@@ -0,0 +1,112 @@
1
+ import type { Duration, Instant, LocalDate, LocalDateTime } from "./time.ts";
2
+ /** The one value of type `Done`: a handler that has nothing to say. */
3
+ export declare const done: Readonly<{
4
+ readonly done: true;
5
+ }>;
6
+ export type Done = typeof done;
7
+ export type ScalarKind = "string" | "int" | "long" | "double" | "boolean" | "instant" | "duration" | "localDate" | "localDateTime" | "bytes" | "unit" | "done";
8
+ /** The structure of a schema, without its type parameter. `resolve` narrows away `lazy`. */
9
+ export type Shape = {
10
+ readonly kind: ScalarKind;
11
+ } | {
12
+ readonly kind: "option";
13
+ readonly inner: Schema;
14
+ } | {
15
+ readonly kind: "list";
16
+ readonly inner: Schema;
17
+ } | {
18
+ readonly kind: "stringMap";
19
+ readonly inner: Schema;
20
+ } | {
21
+ readonly kind: "record";
22
+ readonly name: string;
23
+ readonly fields: Readonly<Record<string, Schema>>;
24
+ } | {
25
+ readonly kind: "sumType";
26
+ readonly name: string;
27
+ readonly cases: Readonly<Record<string, Readonly<Record<string, Schema>>>>;
28
+ } | {
29
+ readonly kind: "enumeration";
30
+ readonly name: string;
31
+ readonly values: readonly string[];
32
+ } | {
33
+ readonly kind: "lazy";
34
+ readonly thunk: () => Schema;
35
+ };
36
+ interface Typed<T> {
37
+ /** Phantom: carries the TypeScript type. Never set. */
38
+ readonly _type?: T;
39
+ }
40
+ /** A shape declaration carrying the TypeScript type it describes. */
41
+ export type Schema<T = unknown> = Typed<T> & Shape;
42
+ /** The TypeScript type a schema describes: `type Cart = Infer<typeof Cart>`. */
43
+ export type Infer<S> = S extends Typed<infer T> ? T : never;
44
+ type Simplify<T> = {
45
+ [K in keyof T]: T[K];
46
+ } & {};
47
+ type InferFields<F extends Readonly<Record<string, Schema>>> = Simplify<{
48
+ [K in keyof F]: Infer<F[K]>;
49
+ }>;
50
+ type InferCases<C extends Readonly<Record<string, Readonly<Record<string, Schema>>>>> = {
51
+ [K in keyof C & string]: Simplify<{
52
+ type: K;
53
+ } & InferFields<C[K]>>;
54
+ }[keyof C & string];
55
+ export declare class SchemaError extends Error {
56
+ constructor(message: string);
57
+ }
58
+ /** The schema builders. `import { s } from "ankka"` and declare shapes with `s.record(...)`. */
59
+ export declare const s: Readonly<{
60
+ /** A JSON string. At top level: the `string` text payload. */
61
+ string: Schema<string>;
62
+ /** A whole number within ±2⁵³, as a JavaScript `number`. At top level: the `int` text payload. */
63
+ int: Schema<number>;
64
+ /** A 64-bit whole number, as a `bigint`, lossless past 2⁵³. At top level: the `long` text payload. */
65
+ long: Schema<bigint>;
66
+ /** A double-precision number, rendered as the Scala codecs render it (`1.0`, `1.0E10`). */
67
+ double: Schema<number>;
68
+ boolean: Schema<boolean>;
69
+ /** ISO-8601 in UTC with 0, 3, 6 or 9 fractional digits. */
70
+ instant: Schema<Instant>;
71
+ /** ISO-8601, `PT1.5S`. */
72
+ duration: Schema<Duration>;
73
+ localDate: Schema<LocalDate>;
74
+ localDateTime: Schema<LocalDateTime>;
75
+ /** Base64 inside JSON; at top level the raw `bytes` payload. */
76
+ bytes: Schema<Uint8Array<ArrayBufferLike>>;
77
+ /** No value: the `unit` payload of zero bytes. */
78
+ unit: Schema<undefined>;
79
+ /** `T | null`: written as `null` when absent; an absent field reads as `null`. */
80
+ option<T>(inner: Schema<T>): Schema<T | null>;
81
+ list<T>(inner: Schema<T>): Schema<T[]>;
82
+ stringMap<T>(inner: Schema<T>): Schema<Record<string, T>>;
83
+ /** A record: a JSON object with every field written, in declaration order. `name` is the default manifest. */
84
+ record<F extends Readonly<Record<string, Schema>>>(name: string, fields: F): Schema<InferFields<F>>;
85
+ /**
86
+ * A sum type: each case is an object carrying `"type": "<CaseName>"` and the case's fields, which is
87
+ * also how a TypeScript discriminated union reads. `s.sumType("Event", { Added: { item: Item }, Done: {} })`
88
+ * gives `{ type: "Added"; item: Item } | { type: "Done" }`.
89
+ */
90
+ sumType<C extends Readonly<Record<string, Readonly<Record<string, Schema>>>>>(name: string, cases: C): Schema<InferCases<C>>;
91
+ /** A fieldless enumeration used as a field: `{"type":"Ready"}` on the wire, `"Ready"` in TypeScript. */
92
+ enumeration<const V extends readonly [string, ...string[]]>(name: string, ...values: V): Schema<V[number]>;
93
+ /** A reference to a schema declared later, for recursive shapes: `s.list(s.lazy(() => Tree))`. */
94
+ lazy<T>(thunk: () => Schema<T>): Schema<T>;
95
+ }>;
96
+ /** The schema of `done`: the empty `done` payload. */
97
+ export declare const Done: Schema<Done>;
98
+ type Resolved<T> = Typed<T> & Exclude<Shape, {
99
+ kind: "lazy";
100
+ }>;
101
+ /** Follows `lazy` references to the schema they name. */
102
+ export declare function resolve<T>(schema: Schema<T>): Resolved<T>;
103
+ /** A short description for error messages: `record Cart`, `list<int>`, `string`. */
104
+ export declare function describe(schema: Schema): string;
105
+ /** The default manifest for a schema: a record's, sum type's or enumeration's name; a scalar's text manifest. */
106
+ export declare function defaultManifest(schema: Schema): string;
107
+ /**
108
+ * JSON Schema (draft 2020-12 vocabulary) for a shape, as an agent tool's input needs it. Objects refuse
109
+ * unknown properties; integers are `integer`; instants are `string` with `format: date-time`.
110
+ */
111
+ export declare function toJsonSchema(schema: Schema): Record<string, unknown>;
112
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,200 @@
1
+ // Shape declarations. TypeScript erases its types, so every value that crosses the protocol is
2
+ // declared once as a `Schema`, and two things are derived from it: the static type (`Infer`) and the
3
+ // codec (`codec.ts`). A schema does what a validation library would not for this encoding: it says
4
+ // whether a number is an `int` or a `double`, carries a `long` as `bigint`, and names records so the
5
+ // default manifest exists. Nothing here performs I/O or touches JSON; that is `json.ts`.
6
+ /** The one value of type `Done`: a handler that has nothing to say. */
7
+ export const done = Object.freeze({ done: true });
8
+ export class SchemaError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "SchemaError";
12
+ }
13
+ }
14
+ function scalar(kind) {
15
+ return Object.freeze({ kind });
16
+ }
17
+ function checkName(what, name) {
18
+ if (typeof name !== "string" || name.trim() === "")
19
+ throw new SchemaError(`a ${what} needs a name`);
20
+ }
21
+ /** The schema builders. `import { s } from "ankka"` and declare shapes with `s.record(...)`. */
22
+ export const s = Object.freeze({
23
+ /** A JSON string. At top level: the `string` text payload. */
24
+ string: scalar("string"),
25
+ /** A whole number within ±2⁵³, as a JavaScript `number`. At top level: the `int` text payload. */
26
+ int: scalar("int"),
27
+ /** A 64-bit whole number, as a `bigint`, lossless past 2⁵³. At top level: the `long` text payload. */
28
+ long: scalar("long"),
29
+ /** A double-precision number, rendered as the Scala codecs render it (`1.0`, `1.0E10`). */
30
+ double: scalar("double"),
31
+ boolean: scalar("boolean"),
32
+ /** ISO-8601 in UTC with 0, 3, 6 or 9 fractional digits. */
33
+ instant: scalar("instant"),
34
+ /** ISO-8601, `PT1.5S`. */
35
+ duration: scalar("duration"),
36
+ localDate: scalar("localDate"),
37
+ localDateTime: scalar("localDateTime"),
38
+ /** Base64 inside JSON; at top level the raw `bytes` payload. */
39
+ bytes: scalar("bytes"),
40
+ /** No value: the `unit` payload of zero bytes. */
41
+ unit: scalar("unit"),
42
+ /** `T | null`: written as `null` when absent; an absent field reads as `null`. */
43
+ option(inner) {
44
+ return Object.freeze({ kind: "option", inner });
45
+ },
46
+ list(inner) {
47
+ return Object.freeze({ kind: "list", inner });
48
+ },
49
+ stringMap(inner) {
50
+ return Object.freeze({ kind: "stringMap", inner });
51
+ },
52
+ /** A record: a JSON object with every field written, in declaration order. `name` is the default manifest. */
53
+ record(name, fields) {
54
+ checkName("record", name);
55
+ return Object.freeze({ kind: "record", name, fields: Object.freeze({ ...fields }) });
56
+ },
57
+ /**
58
+ * A sum type: each case is an object carrying `"type": "<CaseName>"` and the case's fields, which is
59
+ * also how a TypeScript discriminated union reads. `s.sumType("Event", { Added: { item: Item }, Done: {} })`
60
+ * gives `{ type: "Added"; item: Item } | { type: "Done" }`.
61
+ */
62
+ sumType(name, cases) {
63
+ checkName("sum type", name);
64
+ const names = Object.keys(cases);
65
+ if (names.length === 0)
66
+ throw new SchemaError(`sum type ${name} has no cases`);
67
+ for (const c of names) {
68
+ if (c === "type")
69
+ throw new SchemaError(`sum type ${name}: a case cannot be named "type", the discriminator`);
70
+ if ("type" in cases[c])
71
+ throw new SchemaError(`sum type ${name}, case ${c}: a field cannot be named "type", the discriminator`);
72
+ }
73
+ const frozen = {};
74
+ for (const c of names)
75
+ frozen[c] = Object.freeze({ ...cases[c] });
76
+ return Object.freeze({ kind: "sumType", name, cases: Object.freeze(frozen) });
77
+ },
78
+ /** A fieldless enumeration used as a field: `{"type":"Ready"}` on the wire, `"Ready"` in TypeScript. */
79
+ enumeration(name, ...values) {
80
+ checkName("enumeration", name);
81
+ if (new Set(values).size !== values.length)
82
+ throw new SchemaError(`enumeration ${name} repeats a value`);
83
+ return Object.freeze({ kind: "enumeration", name, values: Object.freeze([...values]) });
84
+ },
85
+ /** A reference to a schema declared later, for recursive shapes: `s.list(s.lazy(() => Tree))`. */
86
+ lazy(thunk) {
87
+ return Object.freeze({ kind: "lazy", thunk });
88
+ },
89
+ });
90
+ /** The schema of `done`: the empty `done` payload. */
91
+ export const Done = scalar("done");
92
+ /** Follows `lazy` references to the schema they name. */
93
+ export function resolve(schema) {
94
+ let current = schema;
95
+ for (let i = 0; i < 64 && current.kind === "lazy"; i++)
96
+ current = current.thunk();
97
+ if (current.kind === "lazy")
98
+ throw new SchemaError("a lazy schema never resolves");
99
+ return current;
100
+ }
101
+ /** A short description for error messages: `record Cart`, `list<int>`, `string`. */
102
+ export function describe(schema) {
103
+ const r = resolve(schema);
104
+ switch (r.kind) {
105
+ case "record":
106
+ return `record ${r.name}`;
107
+ case "sumType":
108
+ return `sum type ${r.name}`;
109
+ case "enumeration":
110
+ return `enumeration ${r.name}`;
111
+ case "option":
112
+ return `option<${describe(r.inner)}>`;
113
+ case "list":
114
+ return `list<${describe(r.inner)}>`;
115
+ case "stringMap":
116
+ return `map<string, ${describe(r.inner)}>`;
117
+ default:
118
+ return r.kind;
119
+ }
120
+ }
121
+ /** The default manifest for a schema: a record's, sum type's or enumeration's name; a scalar's text manifest. */
122
+ export function defaultManifest(schema) {
123
+ const r = resolve(schema);
124
+ switch (r.kind) {
125
+ case "record":
126
+ case "sumType":
127
+ case "enumeration":
128
+ return r.name;
129
+ case "option":
130
+ return `option[${defaultManifest(r.inner)}]`;
131
+ case "list":
132
+ return `list[${defaultManifest(r.inner)}]`;
133
+ case "stringMap":
134
+ return `map[${defaultManifest(r.inner)}]`;
135
+ default:
136
+ return r.kind;
137
+ }
138
+ }
139
+ /**
140
+ * JSON Schema (draft 2020-12 vocabulary) for a shape, as an agent tool's input needs it. Objects refuse
141
+ * unknown properties; integers are `integer`; instants are `string` with `format: date-time`.
142
+ */
143
+ export function toJsonSchema(schema) {
144
+ const r = resolve(schema);
145
+ switch (r.kind) {
146
+ case "string":
147
+ return { type: "string" };
148
+ case "int":
149
+ case "long":
150
+ return { type: "integer" };
151
+ case "double":
152
+ return { type: "number" };
153
+ case "boolean":
154
+ return { type: "boolean" };
155
+ case "instant":
156
+ return { type: "string", format: "date-time" };
157
+ case "duration":
158
+ return { type: "string", format: "duration" };
159
+ case "localDate":
160
+ return { type: "string", format: "date" };
161
+ case "localDateTime":
162
+ return { type: "string", format: "date-time" };
163
+ case "bytes":
164
+ return { type: "string", contentEncoding: "base64" };
165
+ case "unit":
166
+ case "done":
167
+ return { type: "null" };
168
+ case "option":
169
+ return { anyOf: [toJsonSchema(r.inner), { type: "null" }] };
170
+ case "list":
171
+ return { type: "array", items: toJsonSchema(r.inner) };
172
+ case "stringMap":
173
+ return { type: "object", additionalProperties: toJsonSchema(r.inner) };
174
+ case "record":
175
+ return objectSchema(r.fields);
176
+ case "sumType":
177
+ return {
178
+ oneOf: Object.entries(r.cases).map(([name, fields]) => {
179
+ const o = objectSchema(fields);
180
+ return {
181
+ ...o,
182
+ properties: { type: { const: name }, ...o.properties },
183
+ required: ["type", ...o.required],
184
+ };
185
+ }),
186
+ };
187
+ case "enumeration":
188
+ return { type: "object", properties: { type: { enum: [...r.values] } }, required: ["type"], additionalProperties: false };
189
+ }
190
+ }
191
+ function objectSchema(fields) {
192
+ const properties = {};
193
+ const required = [];
194
+ for (const [name, field] of Object.entries(fields)) {
195
+ properties[name] = toJsonSchema(field);
196
+ if (resolve(field).kind !== "option")
197
+ required.push(name);
198
+ }
199
+ return { type: "object", properties, required, additionalProperties: false };
200
+ }
@@ -0,0 +1,14 @@
1
+ import type { ConnectRouter } from "@connectrpc/connect";
2
+ import { type GuardrailRequest, type GuardrailResult, type PlanReply, type PlanRequest, type ToolRequest, type ToolResult } from "../_proto/ankka/protocol/v1/agent_pb.ts";
3
+ import type { AgentEffect } from "../effects/agent.ts";
4
+ import type { Agent } from "../agent.ts";
5
+ import type { RegisteredAgent } from "../service.ts";
6
+ import type { ServerContext } from "./server.ts";
7
+ /** Checks a plan against the declaration: a plan may only name tools and guardrails discovery declared. */
8
+ export declare function checkPlan(registered: RegisteredAgent, effect: AgentEffect<unknown>): string | undefined;
9
+ export declare function handlePlan(req: PlanRequest, ctx: ServerContext): Promise<PlanReply>;
10
+ /** Runs a tool: the model's arguments, as JSON text, decoded with the tool's input shape; the result as text for the model. */
11
+ export declare function runTool(registered: RegisteredAgent, agent: Agent, name: string, argumentsJson: string): Promise<string>;
12
+ export declare function handleTool(req: ToolRequest, ctx: ServerContext): Promise<ToolResult>;
13
+ export declare function handleGuardrail(req: GuardrailRequest, ctx: ServerContext): Promise<GuardrailResult>;
14
+ export declare function agentRoutes(router: ConnectRouter, ctx: ServerContext): void;
@@ -0,0 +1,113 @@
1
+ // The agent conversation: the sidecar asks this process to plan (an AgentEffect as data), to run a tool
2
+ // with the model's arguments, and to check a guardrail. The loop, the memory and the model's key are
3
+ // the sidecar's.
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { Agent as AgentService, AgentPlan_Memory, GuardrailRequest_Stage, GuardrailResultSchema, PlanReplySchema, ToolResultSchema, } from "../_proto/ankka/protocol/v1/agent_pb.js";
6
+ import { codecFor, isCodec } from "../codec.js";
7
+ import { metadataFromProto } from "../context.js";
8
+ import { ErrorCode } from "../effects/common.js";
9
+ import { decodeJsonValue, reviver } from "../json.js";
10
+ import { errorCodeToProto } from "../kinds.js";
11
+ import { decodePayload } from "./payloads.js";
12
+ function messageOf(e) {
13
+ return e instanceof Error ? e.message : String(e);
14
+ }
15
+ function planFailure(commandId, error) {
16
+ return create(PlanReplySchema, { message: { case: "failure", value: { commandId, error: { message: error.message, code: errorCodeToProto(error.code) } } } });
17
+ }
18
+ /** Checks a plan against the declaration: a plan may only name tools and guardrails discovery declared. */
19
+ export function checkPlan(registered, effect) {
20
+ const unknown = [...effect.toolNames.filter((t) => !registered.tools.has(t)).map((t) => `tool ${JSON.stringify(t)}`), ...effect.guardrailNames.filter((g) => !registered.guardrails.has(g)).map((g) => `guardrail ${JSON.stringify(g)}`)];
21
+ return unknown.length > 0 ? `${registered.id}: the plan names undeclared ${unknown.join(", ")}` : undefined;
22
+ }
23
+ export async function handlePlan(req, ctx) {
24
+ const registered = ctx.registry.of("agent", req.componentId);
25
+ if (!registered)
26
+ return planFailure(0n, { message: `no agent ${JSON.stringify(req.componentId)} is registered`, code: ErrorCode.NotFound });
27
+ const handler = registered.handlers.get(req.name);
28
+ if (!handler)
29
+ return planFailure(0n, { message: `no handler ${JSON.stringify(req.name)} on ${registered.id}`, code: ErrorCode.NotFound });
30
+ const metadata = metadataFromProto(req.metadata);
31
+ const agent = new registered.cls();
32
+ agent._bind(req.sessionId, metadata, ctx.client.withMetadata(metadata));
33
+ let effect;
34
+ try {
35
+ const input = handler.input ? decodePayload(codecFor(handler.input), req.payload) : undefined;
36
+ effect = (await handler.run(agent, input));
37
+ if (effect?.kind !== "agent")
38
+ throw new TypeError(`${registered.id}/${req.name} returned something that is not an agent effect`);
39
+ const problem = checkPlan(registered, effect);
40
+ if (problem)
41
+ throw new Error(problem);
42
+ }
43
+ catch (e) {
44
+ ctx.log(`ankka: agent ${registered.id}/${req.sessionId}/${req.name} threw: ${messageOf(e)}`);
45
+ return planFailure(0n, { message: messageOf(e), code: ErrorCode.Internal });
46
+ }
47
+ return create(PlanReplySchema, {
48
+ message: {
49
+ case: "plan",
50
+ value: {
51
+ ...(effect.model !== null ? { model: effect.model } : {}),
52
+ ...(effect.system !== null ? { system: effect.system } : {}),
53
+ ...(effect.user !== null ? { user: effect.user } : {}),
54
+ context: [...effect.context],
55
+ memory: effect.sessionMemory ? AgentPlan_Memory.SESSION : AgentPlan_Memory.NONE,
56
+ tools: [...effect.toolNames],
57
+ guardrails: [...effect.guardrailNames],
58
+ responseShape: effect.jsonReply ? { shape: { case: "json", value: { schemaHint: effect.schemaHint } } } : { shape: { case: "text", value: {} } },
59
+ ...(effect.failure ? { failure: { message: effect.failure.message, code: errorCodeToProto(effect.failure.code) } } : {}),
60
+ },
61
+ },
62
+ });
63
+ }
64
+ /** Runs a tool: the model's arguments, as JSON text, decoded with the tool's input shape; the result as text for the model. */
65
+ export async function runTool(registered, agent, name, argumentsJson) {
66
+ const tool = registered.tools.get(name);
67
+ if (!tool)
68
+ throw new Error(`no tool ${JSON.stringify(name)} on ${registered.id}`);
69
+ let input;
70
+ if (isCodec(tool.input))
71
+ input = tool.input.decode(new TextEncoder().encode(argumentsJson));
72
+ else
73
+ input = decodeJsonValue(tool.input, JSON.parse(argumentsJson || "{}", reviver));
74
+ const result = await tool.run(agent, input);
75
+ return typeof result === "string" ? result : JSON.stringify(result, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
76
+ }
77
+ export async function handleTool(req, ctx) {
78
+ const registered = ctx.registry.of("agent", req.componentId);
79
+ if (!registered)
80
+ return create(ToolResultSchema, { result: { case: "error", value: `no agent ${JSON.stringify(req.componentId)} is registered` } });
81
+ const agent = new registered.cls();
82
+ agent._bind(req.sessionId, {}, ctx.client);
83
+ try {
84
+ return create(ToolResultSchema, { result: { case: "ok", value: await runTool(registered, agent, req.tool, req.argumentsJson) } });
85
+ }
86
+ catch (e) {
87
+ // A tool error is fed back to the model, which usually corrects itself.
88
+ return create(ToolResultSchema, { result: { case: "error", value: messageOf(e) } });
89
+ }
90
+ }
91
+ export async function handleGuardrail(req, ctx) {
92
+ const registered = ctx.registry.of("agent", req.componentId);
93
+ const guardrail = registered?.guardrails.get(req.guardrail);
94
+ if (!registered || !guardrail)
95
+ return create(GuardrailResultSchema, { result: { case: "block", value: `no guardrail ${JSON.stringify(req.guardrail)} on ${req.componentId}` } });
96
+ try {
97
+ const reason = await guardrail.check(req.stage === GuardrailRequest_Stage.OUTPUT ? "output" : "input", req.text);
98
+ return reason === null || reason === undefined
99
+ ? create(GuardrailResultSchema, { result: { case: "pass", value: {} } })
100
+ : create(GuardrailResultSchema, { result: { case: "block", value: reason } });
101
+ }
102
+ catch (e) {
103
+ ctx.log(`ankka: guardrail ${registered.id}/${req.guardrail} threw: ${messageOf(e)}`);
104
+ return create(GuardrailResultSchema, { result: { case: "block", value: `guardrail ${req.guardrail} failed: ${messageOf(e)}` } });
105
+ }
106
+ }
107
+ export function agentRoutes(router, ctx) {
108
+ router.service(AgentService, {
109
+ plan: (req) => handlePlan(req, ctx),
110
+ invokeTool: (req) => handleTool(req, ctx),
111
+ checkGuardrail: (req) => handleGuardrail(req, ctx),
112
+ });
113
+ }
@@ -0,0 +1,5 @@
1
+ import type { ConnectRouter } from "@connectrpc/connect";
2
+ import { type Spec } from "../_proto/ankka/protocol/v1/discovery_pb.ts";
3
+ /** Every problem the sidecar has reported to this process, in order. */
4
+ export declare const problems: string[];
5
+ export declare function discoveryRoutes(router: ConnectRouter, spec: () => Spec, log?: (message: string) => void): void;
@@ -0,0 +1,21 @@
1
+ // Discovery: the first conversation. The sidecar asks what we host; we answer with the Spec the
2
+ // registry renders. `ReportError` is how the sidecar tells us why it refused to start — logged, and kept
3
+ // so a service can expose it (the conformance reference does, at GET /conformance/problems).
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { Discovery } from "../_proto/ankka/protocol/v1/discovery_pb.js";
6
+ import { EmptySchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
7
+ /** Every problem the sidecar has reported to this process, in order. */
8
+ export const problems = [];
9
+ export function discoveryRoutes(router, spec, log = console.error) {
10
+ router.service(Discovery, {
11
+ async discover(info) {
12
+ log(`ankka: discovery from sidecar (protocol ${info.protocolVersion}, runtime ${info.runtimeVersion})`);
13
+ return spec();
14
+ },
15
+ async reportError(problem) {
16
+ log(`ankka: the sidecar refused to start: ${problem.message}`);
17
+ problems.push(problem.message);
18
+ return create(EmptySchema);
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { ConnectRouter } from "@connectrpc/connect";
2
+ import { type MessageInitShape } from "@bufbuild/protobuf";
3
+ import { type EventSourcedIn, type EventSourcedOut } from "../_proto/ankka/protocol/v1/event_sourced_pb.ts";
4
+ import type { RetentionSchema } from "../_proto/ankka/protocol/v1/payload_pb.ts";
5
+ import { type Retention } from "../effects/common.ts";
6
+ import type { ServerContext } from "./server.ts";
7
+ type RetentionInit = MessageInitShape<typeof RetentionSchema>;
8
+ /** Retention as the protocol carries it; `undefined` when the effect asked for none. */
9
+ export declare function retentionToProto(retention: Retention | null): RetentionInit | undefined;
10
+ export declare function eventSourcedRoutes(router: ConnectRouter, ctx: ServerContext): void;
11
+ export declare function handleEventSourced(requests: AsyncIterable<EventSourcedIn>, ctx: ServerContext): AsyncIterable<EventSourcedOut>;
12
+ export {};