ankka 0.10.0 → 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/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 +24 -2
- package/dist/consumer.js +20 -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/endpoint.d.ts +10 -1
- package/dist/endpoint.js +20 -0
- package/dist/graph.d.ts +15 -1
- package/dist/graph.js +20 -0
- package/dist/handlers.d.ts +10 -2
- package/dist/handlers.js +6 -2
- package/dist/index.d.ts +12 -4
- package/dist/index.js +11 -3
- 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 +38 -0
- package/dist/server/workflow.js +5 -1
- package/dist/service.d.ts +40 -2
- package/dist/service.js +209 -14
- 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 +43 -2
- package/dist/testkit/kinds.js +196 -26
- 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
package/dist/service.js
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
// time, and reported together before anything listens — the same rule as the Scala builder.
|
|
4
4
|
//
|
|
5
5
|
// await Ankka.service().register(ShoppingCartEntity).register(ShoppingCartEndpoint).listen()
|
|
6
|
+
import { isStartFrom } from "./startFrom.js";
|
|
7
|
+
import { Contract } from "./contract.js";
|
|
6
8
|
import { codecFor, isCodec } from "./codec.js";
|
|
7
9
|
import { EventSourcedEntity } from "./eventSourcedEntity.js";
|
|
8
10
|
import { KeyValueEntity } from "./keyValueEntity.js";
|
|
9
11
|
import { Workflow } from "./workflow.js";
|
|
10
12
|
import { View } from "./view.js";
|
|
13
|
+
import { KeyedView } from "./keyedView.js";
|
|
11
14
|
import { Consumer } from "./consumer.js";
|
|
12
15
|
import { GraphConsumer, graphDeltaCodec } from "./graph.js";
|
|
13
16
|
import { TimedAction } from "./timedAction.js";
|
|
14
17
|
import { Agent } from "./agent.js";
|
|
18
|
+
import { serverProblems, TOOL_PREFIX } from "./mcp.js";
|
|
15
19
|
import { AutonomousAgent, COMPLETE_TASK, FAIL_TASK } from "./autonomous.js";
|
|
16
20
|
import { Endpoint } from "./endpoint.js";
|
|
17
21
|
import { isAcl } from "./routes.js";
|
|
@@ -96,6 +100,8 @@ export class ServiceBuilder {
|
|
|
96
100
|
add(registerWorkflow(cls, problems), cls);
|
|
97
101
|
else if (extendsBase(cls, View))
|
|
98
102
|
add(registerView(cls, problems), cls);
|
|
103
|
+
else if (extendsBase(cls, KeyedView))
|
|
104
|
+
add(registerKeyedView(cls, problems), cls);
|
|
99
105
|
else if (extendsBase(cls, Consumer))
|
|
100
106
|
add(registerConsumer(cls, problems), cls);
|
|
101
107
|
else if (extendsBase(cls, GraphConsumer))
|
|
@@ -123,7 +129,7 @@ export class ServiceBuilder {
|
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
else {
|
|
126
|
-
problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, Consumer, GraphConsumer, TimedAction, Agent, AutonomousAgent or Endpoint`);
|
|
132
|
+
problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, KeyedView, Consumer, GraphConsumer, TimedAction, Agent, AutonomousAgent or Endpoint`);
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
if (problems.length > 0)
|
|
@@ -223,19 +229,59 @@ function registerWorkflow(cls, problems) {
|
|
|
223
229
|
return undefined;
|
|
224
230
|
return Object.freeze({ kind: "workflow", id: cls.componentId, cls, stateCodec: codecFor(cls.state), handlers, steps, settings });
|
|
225
231
|
}
|
|
226
|
-
|
|
232
|
+
/**
|
|
233
|
+
* What a view or consumer says about the topic it reads, checked where its other declarations are:
|
|
234
|
+
* a consumer must say where it starts, and a start position means something only for a topic. A
|
|
235
|
+
* version means something for a view whatever it reads — raising it rebuilds the view, from a topic
|
|
236
|
+
* as far back as the broker retains and from an entity from its first event — and for a consumer only
|
|
237
|
+
* over a topic, whose group it renames.
|
|
238
|
+
*/
|
|
239
|
+
function topicProblems(cls, consumer, fail) {
|
|
240
|
+
const readsTopic = cls.source === undefined && cls.topic !== undefined;
|
|
241
|
+
if (cls.startFrom !== undefined && !isStartFrom(cls.startFrom))
|
|
242
|
+
fail("startFrom must be StartFrom.earliest, StartFrom.latest or StartFrom.at(...)");
|
|
243
|
+
if (cls.startFrom !== undefined && !readsTopic)
|
|
244
|
+
fail("declares a start position, which applies to a topic; it reads a component");
|
|
245
|
+
if (consumer && readsTopic && cls.startFrom === undefined) {
|
|
246
|
+
fail(`reads topic ${JSON.stringify(cls.topic)} and declares no start position; declare static startFrom = StartFrom.earliest, StartFrom.latest or StartFrom.at(...)`);
|
|
247
|
+
}
|
|
248
|
+
if (cls.version !== undefined) {
|
|
249
|
+
if (typeof cls.version !== "number" || !Number.isSafeInteger(cls.version) || cls.version < 1) {
|
|
250
|
+
fail(`declares version ${String(cls.version)}; a version is a whole number of 1 or more`);
|
|
251
|
+
}
|
|
252
|
+
else if (consumer && !readsTopic)
|
|
253
|
+
fail("declares a version, which applies to a topic; it reads a component");
|
|
254
|
+
}
|
|
255
|
+
if (cls.contract !== undefined && !(cls.contract instanceof Contract))
|
|
256
|
+
fail("contract must be a Contract: Contract.fromFile(path, name)");
|
|
257
|
+
if (cls.broker !== undefined && (typeof cls.broker !== "string" || cls.broker.trim() === ""))
|
|
258
|
+
fail("broker must be the name of a broker the project declares");
|
|
259
|
+
if (cls.parallel !== undefined && typeof cls.parallel !== "boolean")
|
|
260
|
+
fail("parallel must be true or false");
|
|
261
|
+
if ((cls.contract !== undefined || cls.broker !== undefined || cls.parallel !== undefined) && !readsTopic) {
|
|
262
|
+
fail("declares a contract, a broker or parallel, which apply to a topic; it reads a component");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function sourceOf(cls, fail, consumer) {
|
|
227
266
|
const hasSource = cls.source !== undefined;
|
|
228
267
|
const hasTopic = cls.topic !== undefined;
|
|
229
268
|
if (hasSource === hasTopic) {
|
|
230
269
|
fail("needs a static source (a component class) or a static topic, and not both");
|
|
231
270
|
return undefined;
|
|
232
271
|
}
|
|
272
|
+
topicProblems(cls, consumer, fail);
|
|
233
273
|
if (hasTopic) {
|
|
234
274
|
if (typeof cls.topic !== "string" || cls.topic.trim() === "") {
|
|
235
275
|
fail("topic must be a non-empty string");
|
|
236
276
|
return undefined;
|
|
237
277
|
}
|
|
238
|
-
return {
|
|
278
|
+
return {
|
|
279
|
+
topic: cls.topic,
|
|
280
|
+
...(isStartFrom(cls.startFrom) ? { startFrom: cls.startFrom } : {}),
|
|
281
|
+
...(cls.contract instanceof Contract ? { contract: cls.contract } : {}),
|
|
282
|
+
...(typeof cls.broker === "string" ? { broker: cls.broker } : {}),
|
|
283
|
+
...(cls.parallel === true ? { parallel: true } : {}),
|
|
284
|
+
};
|
|
239
285
|
}
|
|
240
286
|
const src = cls.source;
|
|
241
287
|
const kind = src?.prototype?._kind;
|
|
@@ -248,25 +294,112 @@ function sourceOf(cls, fail) {
|
|
|
248
294
|
function registerView(cls, problems) {
|
|
249
295
|
const { fail, ok } = checker(cls, problems);
|
|
250
296
|
requireId(cls, fail);
|
|
251
|
-
const source = sourceOf(cls, fail);
|
|
297
|
+
const source = sourceOf(cls, fail, false);
|
|
252
298
|
requireShape(cls.events, "events: a schema or codec for the source's events", fail);
|
|
253
299
|
requireShape(cls.row, "row: a schema or codec for its rows", fail);
|
|
254
300
|
const queries = cls.queries ?? ["get", "all"];
|
|
255
301
|
if (!Array.isArray(queries) || queries.some((q) => typeof q !== "string" || q.trim() === ""))
|
|
256
302
|
fail("queries must be a list of names");
|
|
303
|
+
const declared = cls.declared ?? [];
|
|
304
|
+
if (!Array.isArray(declared) || declared.some((q) => typeof q?.name !== "string" || q.name.trim() === "" || typeof q?.statement !== "string" || q.statement.trim() === "")) {
|
|
305
|
+
fail("declared must be a list of declaredQuery(name, statement)");
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const names = declared.map((q) => q.name);
|
|
309
|
+
const twice = names.filter((n, i) => names.indexOf(n) !== i);
|
|
310
|
+
if (twice.length > 0)
|
|
311
|
+
fail(`declares the query '${twice[0]}' twice; each query has a name of its own`);
|
|
312
|
+
}
|
|
257
313
|
if (!ok() || !source)
|
|
258
314
|
return undefined;
|
|
259
|
-
return Object.freeze({
|
|
315
|
+
return Object.freeze({
|
|
316
|
+
kind: "view",
|
|
317
|
+
id: cls.componentId,
|
|
318
|
+
cls,
|
|
319
|
+
source,
|
|
320
|
+
eventCodec: codecFor(cls.events),
|
|
321
|
+
rowCodec: codecFor(cls.row),
|
|
322
|
+
queries: Object.freeze([...queries]),
|
|
323
|
+
version: cls.version,
|
|
324
|
+
declared: Object.freeze([...declared]),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function checkVersion(version, fail) {
|
|
328
|
+
if (version !== undefined && (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1)) {
|
|
329
|
+
fail(`declares version ${String(version)}; a version is a whole number of 1 or more`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function checkDeclared(declared, fail) {
|
|
333
|
+
if (declared === undefined)
|
|
334
|
+
return [];
|
|
335
|
+
if (!Array.isArray(declared) || declared.some((q) => typeof q?.name !== "string" || q.name.trim() === "" || typeof q?.statement !== "string" || q.statement.trim() === "")) {
|
|
336
|
+
fail("declared must be a list of declaredQuery(name, statement)");
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
const names = declared.map((q) => q.name);
|
|
340
|
+
const twice = names.filter((n, i) => names.indexOf(n) !== i);
|
|
341
|
+
if (twice.length > 0)
|
|
342
|
+
fail(`declares the query '${twice[0]}' twice; each query has a name of its own`);
|
|
343
|
+
return declared;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A keyed view: one or more entities, each through a handler of its own. A topic is not a keyed view's
|
|
347
|
+
* source, and one entity is read once.
|
|
348
|
+
*/
|
|
349
|
+
function registerKeyedView(cls, problems) {
|
|
350
|
+
const { fail, ok } = checker(cls, problems);
|
|
351
|
+
requireId(cls, fail);
|
|
352
|
+
requireShape(cls.row, "row: a schema or codec for its rows", fail);
|
|
353
|
+
checkVersion(cls.version, fail);
|
|
354
|
+
const declared = checkDeclared(cls.declared, fail);
|
|
355
|
+
const sources = new Map();
|
|
356
|
+
const declaredSources = cls.sources;
|
|
357
|
+
if (!Array.isArray(declaredSources) || declaredSources.length === 0) {
|
|
358
|
+
fail("declares no source; a keyed view reads one or more entities, each declared with on(...)");
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
for (const [i, s] of declaredSources.entries()) {
|
|
362
|
+
const ref = s?.source;
|
|
363
|
+
const kind = ref?.prototype?._kind;
|
|
364
|
+
if (typeof s?.onChange !== "function" || typeof ref?.componentId !== "string" || typeof kind !== "string") {
|
|
365
|
+
fail(`sources[${i}] is not a source: declare it with on(EntityClass, events, handler)`);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (kind !== "event-sourced" && kind !== "key-value") {
|
|
369
|
+
fail(`reads ${kind} ${JSON.stringify(ref.componentId)}; a keyed view reads event sourced and key value entities`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (!isShape(s.events)) {
|
|
373
|
+
fail(`sources[${i}] needs a schema or codec for ${ref.componentId}'s changes`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (sources.has(ref.componentId)) {
|
|
377
|
+
fail(`reads ${JSON.stringify(ref.componentId)} twice; each source is read once`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
sources.set(ref.componentId, Object.freeze({ id: ref.componentId, kind, eventCodec: codecFor(s.events), onChange: s.onChange, deleted: s.deleted }));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!ok())
|
|
384
|
+
return undefined;
|
|
385
|
+
return Object.freeze({
|
|
386
|
+
kind: "keyed-view",
|
|
387
|
+
id: cls.componentId,
|
|
388
|
+
cls,
|
|
389
|
+
sources,
|
|
390
|
+
rowCodec: codecFor(cls.row),
|
|
391
|
+
version: cls.version,
|
|
392
|
+
declared: Object.freeze([...declared]),
|
|
393
|
+
});
|
|
260
394
|
}
|
|
261
395
|
function registerConsumer(cls, problems) {
|
|
262
396
|
const { fail, ok } = checker(cls, problems);
|
|
263
397
|
requireId(cls, fail);
|
|
264
|
-
const source = sourceOf(cls, fail);
|
|
398
|
+
const source = sourceOf(cls, fail, true);
|
|
265
399
|
requireShape(cls.message, "message: a schema or codec for the source's messages", fail);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
fail(`produces to ${JSON.stringify(cls.producesTo)} and needs a static out: the shape of what it produces`);
|
|
400
|
+
const produces = publicationOf(cls.producesTo, fail);
|
|
401
|
+
if (produces !== undefined && !isShape(cls.out))
|
|
402
|
+
fail(`produces to ${JSON.stringify(produces.topic)} and needs a static out: the shape of what it produces`);
|
|
270
403
|
if (!ok() || !source)
|
|
271
404
|
return undefined;
|
|
272
405
|
return Object.freeze({
|
|
@@ -276,15 +409,44 @@ function registerConsumer(cls, problems) {
|
|
|
276
409
|
source,
|
|
277
410
|
messageCodec: codecFor(cls.message),
|
|
278
411
|
outCodec: cls.out ? codecFor(cls.out) : undefined,
|
|
279
|
-
producesTo:
|
|
412
|
+
producesTo: produces?.topic,
|
|
413
|
+
produces,
|
|
280
414
|
graph: false,
|
|
415
|
+
version: cls.version,
|
|
281
416
|
});
|
|
282
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* `producesTo` as declared: a topic name, or a Publication naming the topic with the contract stated
|
|
420
|
+
* for it and its broker. Undefined when the consumer produces nothing.
|
|
421
|
+
*/
|
|
422
|
+
function publicationOf(declared, fail) {
|
|
423
|
+
if (declared === undefined)
|
|
424
|
+
return undefined;
|
|
425
|
+
if (typeof declared === "string") {
|
|
426
|
+
if (declared.trim() === "")
|
|
427
|
+
fail("producesTo must be a topic name");
|
|
428
|
+
return declared.trim() === "" ? undefined : { topic: declared };
|
|
429
|
+
}
|
|
430
|
+
const p = declared;
|
|
431
|
+
if (typeof p !== "object" || p === null || typeof p.topic !== "string" || p.topic.trim() === "") {
|
|
432
|
+
fail("producesTo must be a topic name, or { topic, contract?, broker? }");
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
if (p.contract !== undefined && !(p.contract instanceof Contract))
|
|
436
|
+
fail("producesTo.contract must be a Contract: Contract.fromFile(path, name)");
|
|
437
|
+
if (p.broker !== undefined && (typeof p.broker !== "string" || p.broker.trim() === ""))
|
|
438
|
+
fail("producesTo.broker must be the name of a broker the project declares");
|
|
439
|
+
return {
|
|
440
|
+
topic: p.topic,
|
|
441
|
+
...(p.contract instanceof Contract ? { contract: p.contract } : {}),
|
|
442
|
+
...(typeof p.broker === "string" ? { broker: p.broker } : {}),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
283
445
|
/** A graph consumer is a consumer to discovery and to the runtime: one that produces deltas to its topic. */
|
|
284
446
|
function registerGraphConsumer(cls, problems) {
|
|
285
447
|
const { fail, ok } = checker(cls, problems);
|
|
286
448
|
requireId(cls, fail);
|
|
287
|
-
const source = sourceOf(cls, fail);
|
|
449
|
+
const source = sourceOf(cls, fail, true);
|
|
288
450
|
requireShape(cls.message, "message: a schema or codec for the source's messages", fail);
|
|
289
451
|
if (typeof cls.producesTo !== "string" || cls.producesTo.trim() === "")
|
|
290
452
|
fail("needs a static producesTo: the topic its deltas are published to");
|
|
@@ -300,7 +462,9 @@ function registerGraphConsumer(cls, problems) {
|
|
|
300
462
|
messageCodec: codecFor(cls.message),
|
|
301
463
|
outCodec: graphDeltaCodec,
|
|
302
464
|
producesTo: cls.producesTo,
|
|
465
|
+
produces: undefined,
|
|
303
466
|
graph: true,
|
|
467
|
+
version: cls.version,
|
|
304
468
|
});
|
|
305
469
|
}
|
|
306
470
|
function registerTimedAction(cls, problems) {
|
|
@@ -340,9 +504,10 @@ function registerAgent(cls, problems) {
|
|
|
340
504
|
const maxToolCallSteps = cls.maxToolCallSteps ?? 100;
|
|
341
505
|
if (!Number.isInteger(maxToolCallSteps) || maxToolCallSteps <= 0)
|
|
342
506
|
fail("maxToolCallSteps must be a positive integer");
|
|
507
|
+
const { mcpServers, resultGuardrails } = collectMcp(cls, tools, fail);
|
|
343
508
|
if (!ok())
|
|
344
509
|
return undefined;
|
|
345
|
-
return Object.freeze({ kind: "agent", id: cls.componentId, cls, handlers, tools, guardrails, role: cls.role ?? "", maxToolCallSteps });
|
|
510
|
+
return Object.freeze({ kind: "agent", id: cls.componentId, cls, handlers, tools, guardrails, mcpServers, resultGuardrails, role: cls.role ?? "", maxToolCallSteps });
|
|
346
511
|
}
|
|
347
512
|
function registerAutonomousAgent(cls, problems) {
|
|
348
513
|
const { fail, ok } = checker(cls, problems);
|
|
@@ -378,9 +543,39 @@ function registerAutonomousAgent(cls, problems) {
|
|
|
378
543
|
fail(`task type ${JSON.stringify(a.taskType.name)} needs a budget of at least one iteration`);
|
|
379
544
|
taskTypes.set(a.taskType.name, a.taskType);
|
|
380
545
|
}
|
|
546
|
+
const { mcpServers, resultGuardrails } = collectMcp(cls, tools, fail);
|
|
381
547
|
if (!ok())
|
|
382
548
|
return undefined;
|
|
383
|
-
return Object.freeze({ kind: "autonomous-agent", id: cls.componentId, cls, tools, guardrails, taskTypes });
|
|
549
|
+
return Object.freeze({ kind: "autonomous-agent", id: cls.componentId, cls, tools, guardrails, mcpServers, resultGuardrails, taskTypes });
|
|
550
|
+
}
|
|
551
|
+
/** An agent's MCP servers and result guardrails by name, with what is wrong with them in the sidecar's words. */
|
|
552
|
+
function collectMcp(cls, tools, fail) {
|
|
553
|
+
const mcpServers = new Map();
|
|
554
|
+
for (const [property, server] of Object.entries(cls.mcpServers ?? {})) {
|
|
555
|
+
if (typeof server !== "object" || server === null || typeof server.name !== "string") {
|
|
556
|
+
fail(`mcpServers.${property} is not an MCP server: declare it with mcpServer(name, options)`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
serverProblems(server).forEach(fail);
|
|
560
|
+
if (mcpServers.has(server.name))
|
|
561
|
+
fail(`MCP server '${server.name}' is listed twice`);
|
|
562
|
+
mcpServers.set(server.name, server);
|
|
563
|
+
}
|
|
564
|
+
for (const t of tools.values()) {
|
|
565
|
+
if (t.name.startsWith(TOOL_PREFIX))
|
|
566
|
+
fail(`tool '${t.name}' takes the prefix '${TOOL_PREFIX}', which names an MCP server's tools`);
|
|
567
|
+
}
|
|
568
|
+
const resultGuardrails = new Map();
|
|
569
|
+
for (const [property, g] of Object.entries(cls.resultGuardrails ?? {})) {
|
|
570
|
+
if (typeof g !== "object" || g === null || typeof g.name !== "string" || typeof g.check !== "function") {
|
|
571
|
+
fail(`resultGuardrails.${property} is not a result guardrail: declare it with resultGuardrail(name, check)`);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (resultGuardrails.has(g.name))
|
|
575
|
+
fail(`result guardrail '${g.name}' is declared twice`);
|
|
576
|
+
resultGuardrails.set(g.name, g);
|
|
577
|
+
}
|
|
578
|
+
return { mcpServers, resultGuardrails };
|
|
384
579
|
}
|
|
385
580
|
/** Collects a handler table by wire name, reporting a duplicate wire name or a kind the component cannot host. */
|
|
386
581
|
export function collectHandlers(table, what, allowed, fail) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type ComponentClient } from "./client.ts";
|
|
2
|
+
import { type Shape } from "./codec.ts";
|
|
3
|
+
/** The protocol that added calls to other services. */
|
|
4
|
+
export declare const SERVICES_SINCE = "1.8";
|
|
5
|
+
/** The largest body, either way, a call through the sidecar carries: under gRPC's 4 MiB. */
|
|
6
|
+
export declare const MAX_BODY_BYTES = 4000000;
|
|
7
|
+
/** Headers, in order; a name may appear twice. The platform's own are removed before sending. */
|
|
8
|
+
export type ServiceHeaders = ReadonlyArray<readonly [string, string]>;
|
|
9
|
+
/** What the service answered: its status, content type, body and headers, as it sent them. */
|
|
10
|
+
export interface ServiceResponse {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly contentType: string;
|
|
13
|
+
readonly body: Uint8Array;
|
|
14
|
+
readonly headers: ServiceHeaders;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
}
|
|
17
|
+
/** A call to another service that did not end in an answer a typed helper accepts. */
|
|
18
|
+
export declare class ServiceError extends Error {
|
|
19
|
+
readonly service: string;
|
|
20
|
+
constructor(service: string, message: string);
|
|
21
|
+
}
|
|
22
|
+
/** No such service was found; nothing was sent. `reason` says what was tried. */
|
|
23
|
+
export declare class ServiceUnresolvable extends ServiceError {
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
constructor(service: string, reason: string);
|
|
26
|
+
}
|
|
27
|
+
/** What answered for the name is not the service asked for; nothing was sent. */
|
|
28
|
+
export declare class ServiceIdentityMismatch extends ServiceError {
|
|
29
|
+
readonly detail: string;
|
|
30
|
+
constructor(service: string, detail: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* No answer came: the connection was refused or broke, or the service's timeout passed. The service
|
|
34
|
+
* may have received the request; one that may change something was sent at most once.
|
|
35
|
+
*/
|
|
36
|
+
export declare class ServiceUnanswered extends ServiceError {
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
constructor(service: string, reason: string);
|
|
39
|
+
}
|
|
40
|
+
/** The service answered with a status outside 2xx, to a typed helper. `request` returns every answer. */
|
|
41
|
+
export declare class ServiceCallFailed extends ServiceError {
|
|
42
|
+
readonly status: number;
|
|
43
|
+
readonly body: string;
|
|
44
|
+
constructor(service: string, status: number, body: string);
|
|
45
|
+
}
|
|
46
|
+
/** One request, as a client sends it. */
|
|
47
|
+
export interface ServiceRequest {
|
|
48
|
+
readonly service: string;
|
|
49
|
+
readonly project: string | undefined;
|
|
50
|
+
readonly method: string;
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly headers: ServiceHeaders;
|
|
53
|
+
readonly contentType: string | undefined;
|
|
54
|
+
readonly body: Uint8Array | undefined;
|
|
55
|
+
}
|
|
56
|
+
type Send = (request: ServiceRequest, target: string) => Promise<ServiceResponse>;
|
|
57
|
+
/** Calls one service. `target` is the service as it was named: `name` or `project/name`. */
|
|
58
|
+
export declare class ServiceClient {
|
|
59
|
+
#private;
|
|
60
|
+
readonly target: string;
|
|
61
|
+
/** @internal */
|
|
62
|
+
constructor(send: Send, service: string, project: string | undefined);
|
|
63
|
+
/** One request, answered whatever its status. Throws only when no answer came. */
|
|
64
|
+
request(method: string, path: string, options?: {
|
|
65
|
+
body?: Uint8Array;
|
|
66
|
+
contentType?: string;
|
|
67
|
+
headers?: ServiceHeaders;
|
|
68
|
+
}): Promise<ServiceResponse>;
|
|
69
|
+
/** A JSON answer, decoded as `returns`; a status outside 2xx is a `ServiceCallFailed`. */
|
|
70
|
+
get<R>(path: string, returns: Shape<R>, options?: {
|
|
71
|
+
headers?: ServiceHeaders;
|
|
72
|
+
}): Promise<R>;
|
|
73
|
+
/** A text answer: what an endpoint returning a string sends. */
|
|
74
|
+
getText(path: string, options?: {
|
|
75
|
+
headers?: ServiceHeaders;
|
|
76
|
+
}): Promise<string>;
|
|
77
|
+
post<B, R>(path: string, body: B, shapes: {
|
|
78
|
+
body: Shape<B>;
|
|
79
|
+
returns: Shape<R>;
|
|
80
|
+
}, options?: {
|
|
81
|
+
headers?: ServiceHeaders;
|
|
82
|
+
}): Promise<R>;
|
|
83
|
+
put<B, R>(path: string, body: B, shapes: {
|
|
84
|
+
body: Shape<B>;
|
|
85
|
+
returns: Shape<R>;
|
|
86
|
+
}, options?: {
|
|
87
|
+
headers?: ServiceHeaders;
|
|
88
|
+
}): Promise<R>;
|
|
89
|
+
/** For a route answering nothing: succeeds on any 2xx. */
|
|
90
|
+
delete(path: string, options?: {
|
|
91
|
+
headers?: ServiceHeaders;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* How a component obtains clients for other services: `this.services.service("orders")`, or
|
|
96
|
+
* `this.services.service("billing", "invoices")` for a service in another project — whether it admits
|
|
97
|
+
* this one is its own ACL's decision. The call carries the metadata of the handler that is running, so
|
|
98
|
+
* the runtime makes it as that handler: counted from it, and in its trace.
|
|
99
|
+
*/
|
|
100
|
+
export declare class Services {
|
|
101
|
+
#private;
|
|
102
|
+
/** Calls through `client`'s sidecar, as the handler `client` is scoped to; with none, every call throws. */
|
|
103
|
+
constructor(client?: ComponentClient);
|
|
104
|
+
service(name: string): ServiceClient;
|
|
105
|
+
service(project: string, name: string): ServiceClient;
|
|
106
|
+
/** @internal */
|
|
107
|
+
_send(request: ServiceRequest, target: string): Promise<ServiceResponse>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Other services, for a unit test: each answers as the test scripted it, and every request is
|
|
111
|
+
* recorded. A call to a service nothing is scripted for fails the test, naming the service: a test
|
|
112
|
+
* whose call quietly got a default answer is no longer testing what it says.
|
|
113
|
+
*/
|
|
114
|
+
export declare class ScriptedServices extends Services {
|
|
115
|
+
#private;
|
|
116
|
+
readonly requests: ServiceRequest[];
|
|
117
|
+
constructor();
|
|
118
|
+
/** `name` answers with what `handler` returns; in this service's own project unless one is given. */
|
|
119
|
+
answer(name: string, handler: (request: ServiceRequest) => ServiceResponse, project?: string): this;
|
|
120
|
+
unresolvable(name: string, project?: string): this;
|
|
121
|
+
unanswered(name: string, project?: string): this;
|
|
122
|
+
mismatch(name: string, project?: string): this;
|
|
123
|
+
/** An answer of `body` as `text/plain`, for a script. */
|
|
124
|
+
static text(body: string, status?: number): ServiceResponse;
|
|
125
|
+
/** An answer of `body` as `application/json`, for a script. */
|
|
126
|
+
static json(body: string, status?: number): ServiceResponse;
|
|
127
|
+
/** @internal */
|
|
128
|
+
_send(request: ServiceRequest, target: string): Promise<ServiceResponse>;
|
|
129
|
+
}
|
|
130
|
+
/** Services for a unit test whose every call throws: the unit kit's default, beside `noClient()`. */
|
|
131
|
+
export declare function noServices(): Services;
|
|
132
|
+
/**
|
|
133
|
+
* @internal Other services, reached through `client`'s sidecar as the handler it is scoped to. Not a
|
|
134
|
+
* member of the client, which an entity holds too: only the kinds that may call one call this.
|
|
135
|
+
*/
|
|
136
|
+
export declare function servicesFor(client: ComponentClient): Services;
|
|
137
|
+
export {};
|