ankka 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- 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 +858 -2
- package/dist/_proto/ankka/protocol/v1/client_pb.js +184 -9
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +10 -1
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +373 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +81 -16
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +210 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +35 -5
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +86 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +70 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
- package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -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 +188 -7
- package/dist/client.js +490 -21
- package/dist/consumer.d.ts +42 -3
- package/dist/consumer.js +35 -1
- package/dist/context.d.ts +29 -1
- package/dist/context.js +5 -2
- package/dist/contract.d.ts +40 -0
- package/dist/contract.js +60 -0
- package/dist/effects/common.d.ts +7 -0
- package/dist/effects/common.js +9 -1
- package/dist/effects/eventSourced.d.ts +26 -1
- package/dist/effects/eventSourced.js +58 -5
- package/dist/effects/keyed.d.ts +33 -0
- package/dist/effects/keyed.js +41 -0
- package/dist/endpoint.d.ts +10 -1
- package/dist/endpoint.js +20 -0
- package/dist/eventSourcedEntity.d.ts +8 -3
- package/dist/graph.d.ts +24 -2
- package/dist/graph.js +27 -1
- package/dist/handlers.d.ts +10 -2
- package/dist/handlers.js +6 -2
- package/dist/index.d.ts +15 -6
- package/dist/index.js +12 -3
- package/dist/keyedView.d.ts +59 -0
- package/dist/keyedView.js +73 -0
- package/dist/kinds.js +2 -0
- package/dist/mcp.d.ts +57 -0
- package/dist/mcp.js +64 -0
- package/dist/routes.d.ts +44 -4
- package/dist/routes.js +43 -4
- package/dist/server/agent.js +12 -4
- package/dist/server/discovery.d.ts +17 -1
- package/dist/server/discovery.js +182 -1
- package/dist/server/eventSourced.d.ts +4 -1
- package/dist/server/eventSourced.js +24 -6
- package/dist/server/http.d.ts +2 -1
- package/dist/server/http.js +80 -4
- package/dist/server/stateless.js +42 -3
- package/dist/server/workflow.js +5 -1
- package/dist/service.d.ts +46 -2
- package/dist/service.js +235 -15
- 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 +94 -15
- package/dist/standing.d.ts +18 -0
- package/dist/standing.js +26 -0
- package/dist/startFrom.d.ts +36 -0
- package/dist/startFrom.js +51 -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 +50 -5
- package/dist/testkit/kinds.js +208 -35
- package/dist/testkit/unit.d.ts +41 -3
- package/dist/testkit/unit.js +87 -10
- 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 +48 -1
- package/dist/view.js +18 -1
- package/dist/workflow.d.ts +18 -1
- package/dist/workflow.js +39 -0
- package/package.json +3 -2
package/dist/services.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Other services, called as this one. The runtime beside this process makes the call: it finds the
|
|
2
|
+
// service, presents this service's certificate and checks that what answered is the service asked
|
|
3
|
+
// for. This process never holds a key. Offered to endpoints, workflow steps, consumers, timed actions
|
|
4
|
+
// and agents; an entity and a view have no `services` at all.
|
|
5
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
6
|
+
import { ServiceFailure_Reason } from "./_proto/ankka/protocol/v1/client_pb.js";
|
|
7
|
+
import { channelOf } from "./client.js";
|
|
8
|
+
import { codecFor } from "./codec.js";
|
|
9
|
+
import { CommandError } from "./effects/common.js";
|
|
10
|
+
import { errorCodeFromProto } from "./kinds.js";
|
|
11
|
+
import { metadataToProto } from "./context.js";
|
|
12
|
+
import { PROTOCOL_VERSION } from "./spec.js";
|
|
13
|
+
/** The protocol that added calls to other services. */
|
|
14
|
+
export const SERVICES_SINCE = "1.8";
|
|
15
|
+
/** The largest body, either way, a call through the sidecar carries: under gRPC's 4 MiB. */
|
|
16
|
+
export const MAX_BODY_BYTES = 4_000_000;
|
|
17
|
+
/** A call to another service that did not end in an answer a typed helper accepts. */
|
|
18
|
+
export class ServiceError extends Error {
|
|
19
|
+
service;
|
|
20
|
+
constructor(service, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ServiceError";
|
|
23
|
+
this.service = service;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** No such service was found; nothing was sent. `reason` says what was tried. */
|
|
27
|
+
export class ServiceUnresolvable extends ServiceError {
|
|
28
|
+
reason;
|
|
29
|
+
constructor(service, reason) {
|
|
30
|
+
super(service, `cannot reach ${service}: ${reason}`);
|
|
31
|
+
this.name = "ServiceUnresolvable";
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** What answered for the name is not the service asked for; nothing was sent. */
|
|
36
|
+
export class ServiceIdentityMismatch extends ServiceError {
|
|
37
|
+
detail;
|
|
38
|
+
constructor(service, detail) {
|
|
39
|
+
super(service, `the service reached as ${service} is not it: ${detail}`);
|
|
40
|
+
this.name = "ServiceIdentityMismatch";
|
|
41
|
+
this.detail = detail;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* No answer came: the connection was refused or broke, or the service's timeout passed. The service
|
|
46
|
+
* may have received the request; one that may change something was sent at most once.
|
|
47
|
+
*/
|
|
48
|
+
export class ServiceUnanswered extends ServiceError {
|
|
49
|
+
reason;
|
|
50
|
+
constructor(service, reason) {
|
|
51
|
+
super(service, `${service} did not answer: ${reason}`);
|
|
52
|
+
this.name = "ServiceUnanswered";
|
|
53
|
+
this.reason = reason;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The service answered with a status outside 2xx, to a typed helper. `request` returns every answer. */
|
|
57
|
+
export class ServiceCallFailed extends ServiceError {
|
|
58
|
+
status;
|
|
59
|
+
body;
|
|
60
|
+
constructor(service, status, body) {
|
|
61
|
+
super(service, `${service} answered ${status}: ${body}`);
|
|
62
|
+
this.name = "ServiceCallFailed";
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.body = body;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const decoder = new TextDecoder();
|
|
68
|
+
function response(status, contentType, body, headers) {
|
|
69
|
+
return { status, contentType, body, headers, get text() { return decoder.decode(body); } };
|
|
70
|
+
}
|
|
71
|
+
/** Calls one service. `target` is the service as it was named: `name` or `project/name`. */
|
|
72
|
+
export class ServiceClient {
|
|
73
|
+
target;
|
|
74
|
+
#send;
|
|
75
|
+
#service;
|
|
76
|
+
#project;
|
|
77
|
+
/** @internal */
|
|
78
|
+
constructor(send, service, project) {
|
|
79
|
+
this.#send = send;
|
|
80
|
+
this.#service = service;
|
|
81
|
+
this.#project = project;
|
|
82
|
+
this.target = project ? `${project}/${service}` : service;
|
|
83
|
+
}
|
|
84
|
+
/** One request, answered whatever its status. Throws only when no answer came. */
|
|
85
|
+
async request(method, path, options = {}) {
|
|
86
|
+
if (options.body && options.body.length > MAX_BODY_BYTES) {
|
|
87
|
+
throw new CommandError({
|
|
88
|
+
message: `a call's body is at most ${MAX_BODY_BYTES} bytes through the sidecar; this one is ${options.body.length}`,
|
|
89
|
+
code: "BAD_REQUEST",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return this.#send({
|
|
93
|
+
service: this.#service,
|
|
94
|
+
project: this.#project,
|
|
95
|
+
method,
|
|
96
|
+
path,
|
|
97
|
+
headers: options.headers ?? [],
|
|
98
|
+
contentType: options.contentType,
|
|
99
|
+
body: options.body,
|
|
100
|
+
}, this.target);
|
|
101
|
+
}
|
|
102
|
+
/** A JSON answer, decoded as `returns`; a status outside 2xx is a `ServiceCallFailed`. */
|
|
103
|
+
async get(path, returns, options = {}) {
|
|
104
|
+
return codecFor(returns).decode(this.#succeeded(await this.request("GET", path, options)).body);
|
|
105
|
+
}
|
|
106
|
+
/** A text answer: what an endpoint returning a string sends. */
|
|
107
|
+
async getText(path, options = {}) {
|
|
108
|
+
return this.#succeeded(await this.request("GET", path, options)).text;
|
|
109
|
+
}
|
|
110
|
+
async post(path, body, shapes, options = {}) {
|
|
111
|
+
return this.#withBody("POST", path, body, shapes, options);
|
|
112
|
+
}
|
|
113
|
+
async put(path, body, shapes, options = {}) {
|
|
114
|
+
return this.#withBody("PUT", path, body, shapes, options);
|
|
115
|
+
}
|
|
116
|
+
/** For a route answering nothing: succeeds on any 2xx. */
|
|
117
|
+
async delete(path, options = {}) {
|
|
118
|
+
this.#succeeded(await this.request("DELETE", path, options));
|
|
119
|
+
}
|
|
120
|
+
async #withBody(method, path, body, shapes, options) {
|
|
121
|
+
const codec = codecFor(shapes.body);
|
|
122
|
+
const answer = await this.request(method, path, { body: codec.encode(body), contentType: codec.contentType, headers: options.headers });
|
|
123
|
+
return codecFor(shapes.returns).decode(this.#succeeded(answer).body);
|
|
124
|
+
}
|
|
125
|
+
#succeeded(answer) {
|
|
126
|
+
if (Math.floor(answer.status / 100) !== 2)
|
|
127
|
+
throw new ServiceCallFailed(this.target, answer.status, answer.text);
|
|
128
|
+
return answer;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* How a component obtains clients for other services: `this.services.service("orders")`, or
|
|
133
|
+
* `this.services.service("billing", "invoices")` for a service in another project — whether it admits
|
|
134
|
+
* this one is its own ACL's decision. The call carries the metadata of the handler that is running, so
|
|
135
|
+
* the runtime makes it as that handler: counted from it, and in its trace.
|
|
136
|
+
*/
|
|
137
|
+
export class Services {
|
|
138
|
+
#client;
|
|
139
|
+
/** Calls through `client`'s sidecar, as the handler `client` is scoped to; with none, every call throws. */
|
|
140
|
+
constructor(client) {
|
|
141
|
+
this.#client = client;
|
|
142
|
+
}
|
|
143
|
+
service(first, second) {
|
|
144
|
+
const send = (request, target) => this._send(request, target);
|
|
145
|
+
return second === undefined ? new ServiceClient(send, first, undefined) : new ServiceClient(send, second, first);
|
|
146
|
+
}
|
|
147
|
+
/** @internal */
|
|
148
|
+
async _send(request, target) {
|
|
149
|
+
const channel = this.#client ? channelOf(this.#client) : undefined;
|
|
150
|
+
if (!channel)
|
|
151
|
+
throw new Error("these services are not connected to a runtime; a unit test passes a ScriptedServices");
|
|
152
|
+
const reply = await channel
|
|
153
|
+
.stub()
|
|
154
|
+
.request({
|
|
155
|
+
service: request.service,
|
|
156
|
+
project: request.project,
|
|
157
|
+
method: request.method,
|
|
158
|
+
path: request.path,
|
|
159
|
+
headers: request.headers.map(([name, value]) => ({ name, value })),
|
|
160
|
+
contentType: request.contentType,
|
|
161
|
+
body: request.body,
|
|
162
|
+
metadata: metadataToProto(channel.metadata),
|
|
163
|
+
})
|
|
164
|
+
.catch(tooOld);
|
|
165
|
+
return answerOf(target, reply);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function tooOld(failure) {
|
|
169
|
+
if (failure instanceof ConnectError && failure.code === Code.Unimplemented) {
|
|
170
|
+
throw new CommandError({
|
|
171
|
+
message: `the runtime beside this process cannot call another service, which needs protocol ${SERVICES_SINCE} (this SDK speaks ${PROTOCOL_VERSION}): ${failure.rawMessage}`,
|
|
172
|
+
code: "INTERNAL",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
throw failure;
|
|
176
|
+
}
|
|
177
|
+
function answerOf(target, reply) {
|
|
178
|
+
switch (reply.result.case) {
|
|
179
|
+
case "response": {
|
|
180
|
+
const r = reply.result.value;
|
|
181
|
+
return response(r.status, r.contentType, r.body, r.headers.map((h) => [h.name, h.value]));
|
|
182
|
+
}
|
|
183
|
+
case "failure": {
|
|
184
|
+
const { reason, detail } = reply.result.value;
|
|
185
|
+
if (reason === ServiceFailure_Reason.UNRESOLVABLE)
|
|
186
|
+
throw new ServiceUnresolvable(target, detail);
|
|
187
|
+
if (reason === ServiceFailure_Reason.IDENTITY_MISMATCH)
|
|
188
|
+
throw new ServiceIdentityMismatch(target, detail);
|
|
189
|
+
throw new ServiceUnanswered(target, detail);
|
|
190
|
+
}
|
|
191
|
+
case "error":
|
|
192
|
+
throw new CommandError({ message: reply.result.value.message, code: errorCodeFromProto(reply.result.value.code) });
|
|
193
|
+
default:
|
|
194
|
+
throw new CommandError({ message: `the runtime answered a call to ${target} with nothing`, code: "INTERNAL" });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Other services, for a unit test: each answers as the test scripted it, and every request is
|
|
199
|
+
* recorded. A call to a service nothing is scripted for fails the test, naming the service: a test
|
|
200
|
+
* whose call quietly got a default answer is no longer testing what it says.
|
|
201
|
+
*/
|
|
202
|
+
export class ScriptedServices extends Services {
|
|
203
|
+
requests = [];
|
|
204
|
+
#scripts = new Map();
|
|
205
|
+
constructor() {
|
|
206
|
+
super(undefined);
|
|
207
|
+
}
|
|
208
|
+
/** `name` answers with what `handler` returns; in this service's own project unless one is given. */
|
|
209
|
+
answer(name, handler, project) {
|
|
210
|
+
this.#scripts.set(keyOf(project, name), handler);
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
unresolvable(name, project) {
|
|
214
|
+
this.#scripts.set(keyOf(project, name), new ServiceUnresolvable(keyOf(project, name), "scripted as unresolvable"));
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
unanswered(name, project) {
|
|
218
|
+
this.#scripts.set(keyOf(project, name), new ServiceUnanswered(keyOf(project, name), "scripted as unanswered"));
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
mismatch(name, project) {
|
|
222
|
+
this.#scripts.set(keyOf(project, name), new ServiceIdentityMismatch(keyOf(project, name), "scripted as another service"));
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
/** An answer of `body` as `text/plain`, for a script. */
|
|
226
|
+
static text(body, status = 200) {
|
|
227
|
+
return response(status, "text/plain", new TextEncoder().encode(body), []);
|
|
228
|
+
}
|
|
229
|
+
/** An answer of `body` as `application/json`, for a script. */
|
|
230
|
+
static json(body, status = 200) {
|
|
231
|
+
return response(status, "application/json", new TextEncoder().encode(body), []);
|
|
232
|
+
}
|
|
233
|
+
/** @internal */
|
|
234
|
+
async _send(request, target) {
|
|
235
|
+
this.requests.push(request);
|
|
236
|
+
const script = this.#scripts.get(keyOf(request.project, request.service));
|
|
237
|
+
if (script === undefined) {
|
|
238
|
+
const project = request.project ? `, "${request.project}"` : "";
|
|
239
|
+
throw new Error(`the test called the service '${target}', and nothing is scripted for it: script it with answer("${request.service}", (request) => ScriptedServices.text(...)${project})`);
|
|
240
|
+
}
|
|
241
|
+
if (script instanceof ServiceError)
|
|
242
|
+
throw script;
|
|
243
|
+
return script(request);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function keyOf(project, name) {
|
|
247
|
+
return project ? `${project}/${name}` : name;
|
|
248
|
+
}
|
|
249
|
+
/** Services for a unit test whose every call throws: the unit kit's default, beside `noClient()`. */
|
|
250
|
+
export function noServices() {
|
|
251
|
+
return new Services(undefined);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* @internal Other services, reached through `client`'s sidecar as the handler it is scoped to. Not a
|
|
255
|
+
* member of the client, which an entity holds too: only the kinds that may call one call this.
|
|
256
|
+
*/
|
|
257
|
+
export function servicesFor(client) {
|
|
258
|
+
return new Services(client);
|
|
259
|
+
}
|
package/dist/socket.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Thrown by `Socket.send` once the socket is closed. A handler that lets it escape has ended as a
|
|
2
|
+
* handler ends when its client goes: it is not a failure. */
|
|
3
|
+
export declare class SocketClosed extends Error {
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
/** A socket route handler's hold on its socket: `for await (const text of socket)` reads frames until
|
|
7
|
+
* it is closed, and `await socket.send(text)` writes one. */
|
|
8
|
+
export interface Socket extends AsyncIterable<string> {
|
|
9
|
+
/** The next frame, or `undefined` once the socket is closed. */
|
|
10
|
+
receive(): Promise<string | undefined>;
|
|
11
|
+
/** Sends a frame; rejects with `SocketClosed` once the socket is closed. */
|
|
12
|
+
send(text: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** A socket over two functions: what the servicer and the endpoint test kit each give a handler. */
|
|
15
|
+
export declare function socketOf(receive: () => Promise<string | undefined>, send: (text: string) => Promise<void>): Socket;
|
package/dist/socket.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A socket route's open socket, as its handler holds it. The sidecar holds the socket itself — its
|
|
2
|
+
// access rule, its limits, its close codes — and relays each frame over `Http.HandleSocket`.
|
|
3
|
+
/** Thrown by `Socket.send` once the socket is closed. A handler that lets it escape has ended as a
|
|
4
|
+
* handler ends when its client goes: it is not a failure. */
|
|
5
|
+
export class SocketClosed extends Error {
|
|
6
|
+
constructor(message = "the socket is closed") {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SocketClosed";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** A socket over two functions: what the servicer and the endpoint test kit each give a handler. */
|
|
12
|
+
export function socketOf(receive, send) {
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
receive,
|
|
15
|
+
send,
|
|
16
|
+
[Symbol.asyncIterator]() {
|
|
17
|
+
return {
|
|
18
|
+
async next() {
|
|
19
|
+
const text = await receive();
|
|
20
|
+
return text === undefined ? { value: undefined, done: true } : { value: text, done: false };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
package/dist/spec.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type MessageInitShape } from "@bufbuild/protobuf";
|
|
|
2
2
|
import { Endpoint_Acl, type CallerMatcherSchema, type Spec } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
|
|
3
3
|
import type { Registry } from "./service.ts";
|
|
4
4
|
import type { Acl } from "./routes.ts";
|
|
5
|
-
export declare const PROTOCOL_VERSION = "1.
|
|
5
|
+
export declare const PROTOCOL_VERSION = "1.19";
|
|
6
6
|
export declare const SDK_NAME = "ankka-typescript";
|
|
7
7
|
export declare function aclToProto(acl: Acl): Endpoint_Acl;
|
|
8
8
|
/** The callers a CALLERS acl names, as discovery carries them; empty for every other rule. */
|
package/dist/spec.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
// Renders the discovery Spec from the registry: what the sidecar hosts is exactly what is registered.
|
|
2
2
|
import { create } from "@bufbuild/protobuf";
|
|
3
|
-
import { Endpoint_Acl, SpecSchema } from "./_proto/ankka/protocol/v1/discovery_pb.js";
|
|
3
|
+
import { Endpoint_Acl, Kind, SpecSchema, StartFrom_Named } from "./_proto/ankka/protocol/v1/discovery_pb.js";
|
|
4
4
|
import { resultSchemaJson } from "./autonomous.js";
|
|
5
5
|
import { kindToProto } from "./kinds.js";
|
|
6
6
|
import { toJsonSchema } from "./schema.js";
|
|
7
7
|
import { isCodec } from "./codec.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
|
-
|
|
9
|
+
import { approvalToProto } from "./approvals.js";
|
|
10
|
+
import { serverToProto } from "./mcp.js";
|
|
11
|
+
function toolInit(t) {
|
|
12
|
+
const approval = approvalToProto(t.approval);
|
|
13
|
+
return {
|
|
14
|
+
name: t.name,
|
|
15
|
+
description: t.description,
|
|
16
|
+
inputSchemaJson: JSON.stringify(isCodec(t.input) ? { type: "object" } : toJsonSchema(t.input)),
|
|
17
|
+
...(approval !== undefined ? { approval } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const PROTOCOL_VERSION = "1.19";
|
|
10
21
|
export const SDK_NAME = "ankka-typescript";
|
|
11
22
|
export function aclToProto(acl) {
|
|
12
23
|
if (typeof acl === "object")
|
|
@@ -32,6 +43,8 @@ export function callersToProto(acl) {
|
|
|
32
43
|
return { kind: { case: "anyInProject", value: {} } };
|
|
33
44
|
case "self":
|
|
34
45
|
return { kind: { case: "self", value: {} } };
|
|
46
|
+
case "granted":
|
|
47
|
+
return { kind: { case: "granted", value: {} } };
|
|
35
48
|
case "service":
|
|
36
49
|
return { kind: { case: "service", value: m.project !== undefined ? { name: m.name, project: m.project } : { name: m.name } } };
|
|
37
50
|
}
|
|
@@ -43,9 +56,23 @@ function handlerInits(handlers) {
|
|
|
43
56
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
44
57
|
}
|
|
45
58
|
function sourceInit(source) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
if (!("topic" in source))
|
|
60
|
+
return { source: { case: "component", value: { kind: kindToProto(source.component.kind), id: source.component.id } } };
|
|
61
|
+
const start = source.startFrom;
|
|
62
|
+
// 1.14: the contract, the broker and parallel, absent when not declared.
|
|
63
|
+
const options = {
|
|
64
|
+
...(source.contract !== undefined ? { contract: { name: source.contract.name, fingerprint: source.contract.fingerprint } } : {}),
|
|
65
|
+
...(source.broker !== undefined ? { broker: source.broker } : {}),
|
|
66
|
+
...(source.parallel === true ? { parallel: true } : {}),
|
|
67
|
+
// 1.15: another project's topic.
|
|
68
|
+
...(source.project !== undefined ? { project: source.project } : {}),
|
|
69
|
+
};
|
|
70
|
+
if (start === undefined)
|
|
71
|
+
return { source: { case: "topic", value: source.topic }, ...options };
|
|
72
|
+
const position = start.kind === "at"
|
|
73
|
+
? { case: "atMillis", value: BigInt(start.atMillis) }
|
|
74
|
+
: { case: "named", value: start.kind === "earliest" ? StartFrom_Named.EARLIEST : StartFrom_Named.LATEST };
|
|
75
|
+
return { source: { case: "topic", value: source.topic }, startFrom: { position }, ...options };
|
|
49
76
|
}
|
|
50
77
|
function recoveryInit(r) {
|
|
51
78
|
return { maxRetries: r.maxRetries, ...(r.failoverTo !== undefined ? { failoverTo: r.failoverTo } : {}) };
|
|
@@ -65,12 +92,12 @@ function settingsInit(s) {
|
|
|
65
92
|
};
|
|
66
93
|
}
|
|
67
94
|
function componentInit(c) {
|
|
68
|
-
const base = { kind: kindToProto(c.kind), id: c.id };
|
|
95
|
+
const base = { kind: c.kind === "keyed-view" ? Kind.VIEW : kindToProto(c.kind), id: c.id };
|
|
69
96
|
switch (c.kind) {
|
|
70
97
|
case "autonomous-agent":
|
|
71
98
|
return autonomousInit(c);
|
|
72
99
|
case "event-sourced":
|
|
73
|
-
return { ...base, handlers: handlerInits(c.handlers), detail: { case: "eventSourced", value: { snapshotEvery: c.snapshotEvery } } };
|
|
100
|
+
return { ...base, handlers: handlerInits(c.handlers), detail: { case: "eventSourced", value: { snapshotEvery: c.snapshotEvery, replicated: c.replicated } } };
|
|
74
101
|
case "key-value":
|
|
75
102
|
return { ...base, handlers: handlerInits(c.handlers), detail: { case: "keyValue", value: {} } };
|
|
76
103
|
case "workflow":
|
|
@@ -80,9 +107,60 @@ function componentInit(c) {
|
|
|
80
107
|
detail: { case: "workflow", value: { steps: [...c.steps.keys()].sort(), ...(c.settings ? { settings: settingsInit(c.settings) } : {}) } },
|
|
81
108
|
};
|
|
82
109
|
case "view":
|
|
83
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
...base,
|
|
112
|
+
handlers: [],
|
|
113
|
+
detail: {
|
|
114
|
+
case: "view",
|
|
115
|
+
value: {
|
|
116
|
+
source: sourceInit(c.source),
|
|
117
|
+
rowManifest: c.rowCodec.manifest,
|
|
118
|
+
queries: [...c.queries],
|
|
119
|
+
...(c.version !== undefined ? { version: c.version } : {}),
|
|
120
|
+
declaredQueries: c.declared.map((q) => ({ name: q.name, statement: q.statement, watched: q.watched ?? false })),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
case "keyed-view":
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
handlers: [],
|
|
128
|
+
detail: {
|
|
129
|
+
case: "view",
|
|
130
|
+
value: {
|
|
131
|
+
rowManifest: c.rowCodec.manifest,
|
|
132
|
+
queries: [],
|
|
133
|
+
...(c.version !== undefined ? { version: c.version } : {}),
|
|
134
|
+
sources: [...c.sources.values()].map((s) => sourceInit({ component: { kind: s.kind, id: s.id } })),
|
|
135
|
+
declaredQueries: c.declared.map((q) => ({ name: q.name, statement: q.statement, watched: q.watched ?? false })),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
84
139
|
case "consumer":
|
|
85
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
handlers: [],
|
|
143
|
+
detail: {
|
|
144
|
+
case: "consumer",
|
|
145
|
+
value: {
|
|
146
|
+
source: sourceInit(c.source),
|
|
147
|
+
...(c.producesTo !== undefined ? { producesTo: c.producesTo } : {}),
|
|
148
|
+
...(c.version !== undefined ? { version: c.version } : {}),
|
|
149
|
+
mirroredChanges: c.mirroredChanges,
|
|
150
|
+
// 1.14: the publication with its contract and broker; `producesTo` stays for an older sidecar.
|
|
151
|
+
...(c.produces !== undefined
|
|
152
|
+
? {
|
|
153
|
+
produces: {
|
|
154
|
+
topic: c.produces.topic,
|
|
155
|
+
...(c.produces.contract !== undefined ? { contract: { name: c.produces.contract.name, fingerprint: c.produces.contract.fingerprint } } : {}),
|
|
156
|
+
...(c.produces.broker !== undefined ? { broker: c.produces.broker } : {}),
|
|
157
|
+
...(c.produces.project !== undefined ? { project: c.produces.project } : {}),
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
: {}),
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
86
164
|
case "timed-action":
|
|
87
165
|
return { ...base, handlers: handlerInits(c.actions), detail: { case: "timedAction", value: {} } };
|
|
88
166
|
case "agent":
|
|
@@ -94,10 +172,10 @@ function componentInit(c) {
|
|
|
94
172
|
value: {
|
|
95
173
|
role: c.role,
|
|
96
174
|
maxToolCallSteps: c.maxToolCallSteps,
|
|
97
|
-
tools: [...c.tools.values()]
|
|
98
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
99
|
-
.map((t) => ({ name: t.name, description: t.description, inputSchemaJson: JSON.stringify(isCodec(t.input) ? { type: "object" } : toJsonSchema(t.input)) })),
|
|
175
|
+
tools: [...c.tools.values()].sort((a, b) => a.name.localeCompare(b.name)).map(toolInit),
|
|
100
176
|
guardrails: [...c.guardrails.keys()].sort(),
|
|
177
|
+
mcpServers: [...c.mcpServers.values()].sort((a, b) => a.name.localeCompare(b.name)).map(serverToProto),
|
|
178
|
+
resultGuardrails: [...c.resultGuardrails.keys()].sort(),
|
|
101
179
|
},
|
|
102
180
|
},
|
|
103
181
|
};
|
|
@@ -123,10 +201,10 @@ function autonomousInit(c) {
|
|
|
123
201
|
description: cls.description,
|
|
124
202
|
...(cls.instructions !== undefined ? { instructions: cls.instructions } : {}),
|
|
125
203
|
...(cls.model !== undefined ? { model: cls.model } : {}),
|
|
126
|
-
tools: [...c.tools.values()]
|
|
127
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
128
|
-
.map((t) => ({ name: t.name, description: t.description, inputSchemaJson: JSON.stringify(isCodec(t.input) ? { type: "object" } : toJsonSchema(t.input)) })),
|
|
204
|
+
tools: [...c.tools.values()].sort((a, b) => a.name.localeCompare(b.name)).map(toolInit),
|
|
129
205
|
guardrails: [...c.guardrails.keys()].sort(),
|
|
206
|
+
mcpServers: [...c.mcpServers.values()].sort((a, b) => a.name.localeCompare(b.name)).map(serverToProto),
|
|
207
|
+
resultGuardrails: [...c.resultGuardrails.keys()].sort(),
|
|
130
208
|
taskTypes: [...c.taskTypes.values()].map((t) => {
|
|
131
209
|
const schema = resultSchemaJson(t);
|
|
132
210
|
return { name: t.name, description: t.description, rules: t.rules.map((r) => r.name), ...(schema !== undefined ? { resultSchemaJson: schema } : {}) };
|
|
@@ -150,6 +228,7 @@ export function renderSpec(registry) {
|
|
|
150
228
|
template: r.template,
|
|
151
229
|
hasBody: r.body !== undefined,
|
|
152
230
|
streaming: r.streaming,
|
|
231
|
+
socket: r.socket,
|
|
153
232
|
...(r.acl !== undefined ? { acl: aclToProto(r.acl), allowCallers: callersToProto(r.acl) } : {}),
|
|
154
233
|
})),
|
|
155
234
|
}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkflowStanding } from "./_proto/ankka/protocol/v1/payload_pb.ts";
|
|
2
|
+
export type StandingStatus = "NotStarted" | "Running" | "Paused" | "Completed" | "Failed" | "Unknown";
|
|
3
|
+
export interface Standing {
|
|
4
|
+
readonly status: StandingStatus;
|
|
5
|
+
/** The step the workflow is on, or the step a pause names or follows. */
|
|
6
|
+
readonly step?: string;
|
|
7
|
+
/** Retries so far, per step. */
|
|
8
|
+
readonly retries: Readonly<Record<string, number>>;
|
|
9
|
+
/** Why it failed, when it did. */
|
|
10
|
+
readonly failure?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function isTerminal(standing: Standing): boolean;
|
|
13
|
+
/** The state was recorded before the platform stamped standings. */
|
|
14
|
+
export declare function isUnknown(standing: Standing): boolean;
|
|
15
|
+
/** @internal The standing a request carries, or `undefined` for a change from an entity or a topic. */
|
|
16
|
+
export declare function standingFromProto(standing: WorkflowStanding | undefined): Standing | undefined;
|
|
17
|
+
/** @internal The version whose runtimes read a workflow as a source. */
|
|
18
|
+
export declare const WORKFLOW_SOURCE_PROTOCOL: readonly [number, number];
|
package/dist/standing.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Where a workflow stood once the effect that recorded a state was applied (protocol 1.16).
|
|
2
|
+
//
|
|
3
|
+
// A view or a consumer whose source is a workflow is handed, with each state the workflow records,
|
|
4
|
+
// its standing: running, paused, completed or failed, the step it is on or waits after, the retries
|
|
5
|
+
// of each step and, when it failed, why. `Unknown` is the standing of a state recorded before the
|
|
6
|
+
// platform stamped standings. A change from an entity or a topic has none.
|
|
7
|
+
export function isTerminal(standing) {
|
|
8
|
+
return standing.status === "Completed" || standing.status === "Failed";
|
|
9
|
+
}
|
|
10
|
+
/** The state was recorded before the platform stamped standings. */
|
|
11
|
+
export function isUnknown(standing) {
|
|
12
|
+
return standing.status === "Unknown";
|
|
13
|
+
}
|
|
14
|
+
/** @internal The standing a request carries, or `undefined` for a change from an entity or a topic. */
|
|
15
|
+
export function standingFromProto(standing) {
|
|
16
|
+
if (standing === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
status: standing.status,
|
|
20
|
+
...(standing.step ? { step: standing.step } : {}),
|
|
21
|
+
retries: Object.freeze({ ...standing.retries }),
|
|
22
|
+
...(standing.failure ? { failure: standing.failure } : {}),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** @internal The version whose runtimes read a workflow as a source. */
|
|
26
|
+
export const WORKFLOW_SOURCE_PROTOCOL = [1, 16];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a topic source begins, the first time its consumer group reads a partition. Applied once and
|
|
3
|
+
* committed at once, so a restart, a rebalance or a new instance resumes where the group got to,
|
|
4
|
+
* never from here again.
|
|
5
|
+
*/
|
|
6
|
+
export type StartFrom = {
|
|
7
|
+
readonly kind: "earliest";
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "latest";
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "at";
|
|
12
|
+
readonly atMillis: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const StartFrom: Readonly<{
|
|
15
|
+
/** The oldest message the broker still holds. */
|
|
16
|
+
earliest: StartFrom;
|
|
17
|
+
/** After the newest: only what is published from then on. */
|
|
18
|
+
latest: StartFrom;
|
|
19
|
+
/** The first message published at or after `when`. */
|
|
20
|
+
at(when: Date): StartFrom;
|
|
21
|
+
}>;
|
|
22
|
+
/** The first protocol in which a process can declare a start position or a version. */
|
|
23
|
+
export declare const START_POSITION_PROTOCOL: readonly [number, number];
|
|
24
|
+
export declare function isStartFrom(value: unknown): value is StartFrom;
|
|
25
|
+
/** The first protocol in which a process can declare a view's queries. */
|
|
26
|
+
export declare const DECLARED_QUERY_PROTOCOL: readonly [number, number];
|
|
27
|
+
export declare function olderThan(protocolVersion: string, [wantMajor, wantMinor]: readonly [number, number]): boolean;
|
|
28
|
+
/** Whether a sidecar speaking `protocolVersion` would ignore a start position and a version. */
|
|
29
|
+
export declare function olderThanStartPositions(protocolVersion: string): boolean;
|
|
30
|
+
/** Whether a sidecar speaking `protocolVersion` would ignore a view's declared queries. */
|
|
31
|
+
export declare function olderThanDeclaredQueries(protocolVersion: string): boolean;
|
|
32
|
+
/** Whether a sidecar speaking `protocolVersion` would not know a watched query. */
|
|
33
|
+
export declare function olderThanViewStreams(protocolVersion: string): boolean;
|
|
34
|
+
/** Whether a sidecar speaking `protocolVersion` would ignore a topic's contract, broker and parallel reading. */
|
|
35
|
+
export declare function olderThanWorkflowSources(protocolVersion: string): boolean;
|
|
36
|
+
export declare function olderThanContracts(protocolVersion: string): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Where a topic source starts, and what a view or consumer that reads a topic may declare about it.
|
|
2
|
+
export const StartFrom = Object.freeze({
|
|
3
|
+
/** The oldest message the broker still holds. */
|
|
4
|
+
earliest: Object.freeze({ kind: "earliest" }),
|
|
5
|
+
/** After the newest: only what is published from then on. */
|
|
6
|
+
latest: Object.freeze({ kind: "latest" }),
|
|
7
|
+
/** The first message published at or after `when`. */
|
|
8
|
+
at(when) {
|
|
9
|
+
const atMillis = when instanceof Date ? when.getTime() : Number.NaN;
|
|
10
|
+
if (!Number.isFinite(atMillis))
|
|
11
|
+
throw new TypeError(`StartFrom.at needs a valid Date, not ${String(when)}`);
|
|
12
|
+
return Object.freeze({ kind: "at", atMillis });
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
/** The first protocol in which a process can declare a start position or a version. */
|
|
16
|
+
export const START_POSITION_PROTOCOL = [1, 7];
|
|
17
|
+
export function isStartFrom(value) {
|
|
18
|
+
if (typeof value !== "object" || value === null)
|
|
19
|
+
return false;
|
|
20
|
+
const kind = value.kind;
|
|
21
|
+
if (kind === "earliest" || kind === "latest")
|
|
22
|
+
return true;
|
|
23
|
+
return kind === "at" && Number.isFinite(value.atMillis);
|
|
24
|
+
}
|
|
25
|
+
/** The first protocol in which a process can declare a view's queries. */
|
|
26
|
+
export const DECLARED_QUERY_PROTOCOL = [1, 13];
|
|
27
|
+
export function olderThan(protocolVersion, [wantMajor, wantMinor]) {
|
|
28
|
+
const [major, minor] = protocolVersion.split(".").map((part) => Number.parseInt(part, 10));
|
|
29
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor))
|
|
30
|
+
return false;
|
|
31
|
+
return major < wantMajor || (major === wantMajor && minor < wantMinor);
|
|
32
|
+
}
|
|
33
|
+
/** Whether a sidecar speaking `protocolVersion` would ignore a start position and a version. */
|
|
34
|
+
export function olderThanStartPositions(protocolVersion) {
|
|
35
|
+
return olderThan(protocolVersion, START_POSITION_PROTOCOL);
|
|
36
|
+
}
|
|
37
|
+
/** Whether a sidecar speaking `protocolVersion` would ignore a view's declared queries. */
|
|
38
|
+
export function olderThanDeclaredQueries(protocolVersion) {
|
|
39
|
+
return olderThan(protocolVersion, DECLARED_QUERY_PROTOCOL);
|
|
40
|
+
}
|
|
41
|
+
/** Whether a sidecar speaking `protocolVersion` would not know a watched query. */
|
|
42
|
+
export function olderThanViewStreams(protocolVersion) {
|
|
43
|
+
return olderThan(protocolVersion, [1, 17]);
|
|
44
|
+
}
|
|
45
|
+
/** Whether a sidecar speaking `protocolVersion` would ignore a topic's contract, broker and parallel reading. */
|
|
46
|
+
export function olderThanWorkflowSources(protocolVersion) {
|
|
47
|
+
return olderThan(protocolVersion, [1, 16]);
|
|
48
|
+
}
|
|
49
|
+
export function olderThanContracts(protocolVersion) {
|
|
50
|
+
return olderThan(protocolVersion, [1, 14]);
|
|
51
|
+
}
|
package/dist/testkit/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { EventSourcedTestKit, EndpointTestKit, Response, type RequestOptions, type Materialised } from "./unit.ts";
|
|
2
|
-
export { KeyValueTestKit, WorkflowTestKit, ViewTestKit, ConsumerTestKit, GraphConsumerTestKit, TimedActionTestKit, AgentTestKit, ScriptedModel, type WorkflowProgress, type ModelResponse, type ModelCall, type GraphChange, } from "./kinds.ts";
|
|
1
|
+
export { EventSourcedTestKit, EndpointTestKit, Response, type RequestOptions, type Materialised, type SocketRun, type ReplicationMode } from "./unit.ts";
|
|
2
|
+
export { KeyValueTestKit, WorkflowTestKit, ViewTestKit, KeyedViewTestKit, ConsumerTestKit, GraphConsumerTestKit, TimedActionTestKit, AgentTestKit, ScriptedModel, type WorkflowProgress, type ModelResponse, type ModelCall, type GraphChange, } from "./kinds.ts";
|
|
3
3
|
export { AnkkaTestKit, PUBLISHED_SIDECAR, sidecarImage, type AnkkaTestKitOptions, type HttpResponse, type Http, type Beside } from "./integration.ts";
|
package/dist/testkit/index.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
// (Postgres and the real sidecar image in Docker). The integration testkit's Docker dependencies,
|
|
3
3
|
// `testcontainers` and `@testcontainers/postgresql`, are loaded only when `AnkkaTestKit.start` runs.
|
|
4
4
|
export { EventSourcedTestKit, EndpointTestKit, Response } from "./unit.js";
|
|
5
|
-
export { KeyValueTestKit, WorkflowTestKit, ViewTestKit, ConsumerTestKit, GraphConsumerTestKit, TimedActionTestKit, AgentTestKit, ScriptedModel, } from "./kinds.js";
|
|
5
|
+
export { KeyValueTestKit, WorkflowTestKit, ViewTestKit, KeyedViewTestKit, ConsumerTestKit, GraphConsumerTestKit, TimedActionTestKit, AgentTestKit, ScriptedModel, } from "./kinds.js";
|
|
6
6
|
export { AnkkaTestKit, PUBLISHED_SIDECAR, sidecarImage } from "./integration.js";
|