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.
Files changed (94) hide show
  1. package/README.md +1 -0
  2. package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +35 -0
  3. package/dist/_proto/ankka/protocol/v1/agent_pb.js +8 -1
  4. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +858 -2
  5. package/dist/_proto/ankka/protocol/v1/client_pb.js +184 -9
  6. package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +10 -1
  7. package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
  8. package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +373 -0
  9. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +81 -16
  10. package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +210 -0
  11. package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +35 -5
  12. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +86 -0
  13. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +11 -1
  14. package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
  15. package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
  16. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +70 -1
  17. package/dist/_proto/ankka/protocol/v1/view_pb.js +11 -1
  18. package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
  19. package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
  20. package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +7 -0
  21. package/dist/_proto/ankka/protocol/v1/workflow_pb.js +1 -1
  22. package/dist/agent.d.ts +15 -1
  23. package/dist/agent.js +20 -0
  24. package/dist/approvals.d.ts +47 -0
  25. package/dist/approvals.js +57 -0
  26. package/dist/autonomous.d.ts +23 -1
  27. package/dist/autonomous.js +30 -1
  28. package/dist/client.d.ts +188 -7
  29. package/dist/client.js +490 -21
  30. package/dist/consumer.d.ts +42 -3
  31. package/dist/consumer.js +35 -1
  32. package/dist/context.d.ts +29 -1
  33. package/dist/context.js +5 -2
  34. package/dist/contract.d.ts +40 -0
  35. package/dist/contract.js +60 -0
  36. package/dist/effects/common.d.ts +7 -0
  37. package/dist/effects/common.js +9 -1
  38. package/dist/effects/eventSourced.d.ts +26 -1
  39. package/dist/effects/eventSourced.js +58 -5
  40. package/dist/effects/keyed.d.ts +33 -0
  41. package/dist/effects/keyed.js +41 -0
  42. package/dist/endpoint.d.ts +10 -1
  43. package/dist/endpoint.js +20 -0
  44. package/dist/eventSourcedEntity.d.ts +8 -3
  45. package/dist/graph.d.ts +24 -2
  46. package/dist/graph.js +27 -1
  47. package/dist/handlers.d.ts +10 -2
  48. package/dist/handlers.js +6 -2
  49. package/dist/index.d.ts +15 -6
  50. package/dist/index.js +12 -3
  51. package/dist/keyedView.d.ts +59 -0
  52. package/dist/keyedView.js +73 -0
  53. package/dist/kinds.js +2 -0
  54. package/dist/mcp.d.ts +57 -0
  55. package/dist/mcp.js +64 -0
  56. package/dist/routes.d.ts +44 -4
  57. package/dist/routes.js +43 -4
  58. package/dist/server/agent.js +12 -4
  59. package/dist/server/discovery.d.ts +17 -1
  60. package/dist/server/discovery.js +182 -1
  61. package/dist/server/eventSourced.d.ts +4 -1
  62. package/dist/server/eventSourced.js +24 -6
  63. package/dist/server/http.d.ts +2 -1
  64. package/dist/server/http.js +80 -4
  65. package/dist/server/stateless.js +42 -3
  66. package/dist/server/workflow.js +5 -1
  67. package/dist/service.d.ts +46 -2
  68. package/dist/service.js +235 -15
  69. package/dist/services.d.ts +137 -0
  70. package/dist/services.js +259 -0
  71. package/dist/socket.d.ts +15 -0
  72. package/dist/socket.js +25 -0
  73. package/dist/spec.d.ts +1 -1
  74. package/dist/spec.js +94 -15
  75. package/dist/standing.d.ts +18 -0
  76. package/dist/standing.js +26 -0
  77. package/dist/startFrom.d.ts +36 -0
  78. package/dist/startFrom.js +51 -0
  79. package/dist/testkit/index.d.ts +2 -2
  80. package/dist/testkit/index.js +1 -1
  81. package/dist/testkit/integration.js +5 -0
  82. package/dist/testkit/kinds.d.ts +50 -5
  83. package/dist/testkit/kinds.js +208 -35
  84. package/dist/testkit/unit.d.ts +41 -3
  85. package/dist/testkit/unit.js +87 -10
  86. package/dist/timedAction.d.ts +17 -2
  87. package/dist/timedAction.js +33 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/dist/view.d.ts +48 -1
  91. package/dist/view.js +18 -1
  92. package/dist/workflow.d.ts +18 -1
  93. package/dist/workflow.js +39 -0
  94. 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)
@@ -185,7 +191,18 @@ function registerEventSourced(cls, problems) {
185
191
  const handlers = collectHandlers(cls.handlers, "handlers", ["command", "query"], fail);
186
192
  if (!ok())
187
193
  return undefined;
188
- return Object.freeze({ kind: "event-sourced", id: cls.componentId, cls, stateCodec: codecFor(cls.state), eventCodec: codecFor(cls.events), handlers, snapshotEvery });
194
+ if (cls.replicated !== undefined && typeof cls.replicated !== "boolean")
195
+ fail(`replicated must be true or false, not ${String(cls.replicated)}`);
196
+ return Object.freeze({
197
+ kind: "event-sourced",
198
+ id: cls.componentId,
199
+ cls,
200
+ stateCodec: codecFor(cls.state),
201
+ eventCodec: codecFor(cls.events),
202
+ handlers,
203
+ snapshotEvery,
204
+ replicated: cls.replicated === true,
205
+ });
189
206
  }
190
207
  function registerKeyValue(cls, problems) {
191
208
  const { fail, ok } = checker(cls, problems);
@@ -223,19 +240,66 @@ function registerWorkflow(cls, problems) {
223
240
  return undefined;
224
241
  return Object.freeze({ kind: "workflow", id: cls.componentId, cls, stateCodec: codecFor(cls.state), handlers, steps, settings });
225
242
  }
226
- function sourceOf(cls, fail) {
243
+ /**
244
+ * What a view or consumer says about the topic it reads, checked where its other declarations are:
245
+ * a consumer must say where it starts, and a start position means something only for a topic. A
246
+ * version means something for a view whatever it reads — raising it rebuilds the view, from a topic
247
+ * as far back as the broker retains and from an entity from its first event — and for a consumer only
248
+ * over a topic, whose group it renames.
249
+ */
250
+ function topicProblems(cls, consumer, fail) {
251
+ const readsTopic = cls.source === undefined && cls.topic !== undefined;
252
+ if (cls.startFrom !== undefined && !isStartFrom(cls.startFrom))
253
+ fail("startFrom must be StartFrom.earliest, StartFrom.latest or StartFrom.at(...)");
254
+ if (cls.startFrom !== undefined && !readsTopic)
255
+ fail("declares a start position, which applies to a topic; it reads a component");
256
+ if (consumer && readsTopic && cls.startFrom === undefined) {
257
+ fail(`reads topic ${JSON.stringify(cls.topic)} and declares no start position; declare static startFrom = StartFrom.earliest, StartFrom.latest or StartFrom.at(...)`);
258
+ }
259
+ if (cls.version !== undefined) {
260
+ if (typeof cls.version !== "number" || !Number.isSafeInteger(cls.version) || cls.version < 1) {
261
+ fail(`declares version ${String(cls.version)}; a version is a whole number of 1 or more`);
262
+ }
263
+ else if (consumer && !readsTopic)
264
+ fail("declares a version, which applies to a topic; it reads a component");
265
+ }
266
+ if (cls.contract !== undefined && !(cls.contract instanceof Contract))
267
+ fail("contract must be a Contract: Contract.fromFile(path, name)");
268
+ if (cls.broker !== undefined && (typeof cls.broker !== "string" || cls.broker.trim() === ""))
269
+ fail("broker must be the name of a broker the project declares");
270
+ if (cls.parallel !== undefined && typeof cls.parallel !== "boolean")
271
+ fail("parallel must be true or false");
272
+ if ((cls.contract !== undefined || cls.broker !== undefined || cls.parallel !== undefined) && !readsTopic) {
273
+ fail("declares a contract, a broker or parallel, which apply to a topic; it reads a component");
274
+ }
275
+ if (cls.project !== undefined && (typeof cls.project !== "string" || cls.project.trim() === ""))
276
+ fail("project must be the id of the project whose topic this is");
277
+ if (cls.project !== undefined && !readsTopic)
278
+ fail("names a project, which applies to a topic; it reads a component");
279
+ if (cls.project !== undefined && cls.broker !== undefined)
280
+ fail("names a project and a broker; another project's topic is on the installation's broker");
281
+ }
282
+ function sourceOf(cls, fail, consumer) {
227
283
  const hasSource = cls.source !== undefined;
228
284
  const hasTopic = cls.topic !== undefined;
229
285
  if (hasSource === hasTopic) {
230
286
  fail("needs a static source (a component class) or a static topic, and not both");
231
287
  return undefined;
232
288
  }
289
+ topicProblems(cls, consumer, fail);
233
290
  if (hasTopic) {
234
291
  if (typeof cls.topic !== "string" || cls.topic.trim() === "") {
235
292
  fail("topic must be a non-empty string");
236
293
  return undefined;
237
294
  }
238
- return { topic: cls.topic };
295
+ return {
296
+ topic: cls.topic,
297
+ ...(isStartFrom(cls.startFrom) ? { startFrom: cls.startFrom } : {}),
298
+ ...(cls.contract instanceof Contract ? { contract: cls.contract } : {}),
299
+ ...(typeof cls.broker === "string" ? { broker: cls.broker } : {}),
300
+ ...(cls.parallel === true ? { parallel: true } : {}),
301
+ ...(typeof cls.project === "string" ? { project: cls.project } : {}),
302
+ };
239
303
  }
240
304
  const src = cls.source;
241
305
  const kind = src?.prototype?._kind;
@@ -248,25 +312,112 @@ function sourceOf(cls, fail) {
248
312
  function registerView(cls, problems) {
249
313
  const { fail, ok } = checker(cls, problems);
250
314
  requireId(cls, fail);
251
- const source = sourceOf(cls, fail);
315
+ const source = sourceOf(cls, fail, false);
252
316
  requireShape(cls.events, "events: a schema or codec for the source's events", fail);
253
317
  requireShape(cls.row, "row: a schema or codec for its rows", fail);
254
318
  const queries = cls.queries ?? ["get", "all"];
255
319
  if (!Array.isArray(queries) || queries.some((q) => typeof q !== "string" || q.trim() === ""))
256
320
  fail("queries must be a list of names");
321
+ const declared = cls.declared ?? [];
322
+ if (!Array.isArray(declared) || declared.some((q) => typeof q?.name !== "string" || q.name.trim() === "" || typeof q?.statement !== "string" || q.statement.trim() === "")) {
323
+ fail("declared must be a list of declaredQuery(name, statement)");
324
+ }
325
+ else {
326
+ const names = declared.map((q) => q.name);
327
+ const twice = names.filter((n, i) => names.indexOf(n) !== i);
328
+ if (twice.length > 0)
329
+ fail(`declares the query '${twice[0]}' twice; each query has a name of its own`);
330
+ }
257
331
  if (!ok() || !source)
258
332
  return undefined;
259
- return Object.freeze({ kind: "view", id: cls.componentId, cls, source, eventCodec: codecFor(cls.events), rowCodec: codecFor(cls.row), queries: Object.freeze([...queries]) });
333
+ return Object.freeze({
334
+ kind: "view",
335
+ id: cls.componentId,
336
+ cls,
337
+ source,
338
+ eventCodec: codecFor(cls.events),
339
+ rowCodec: codecFor(cls.row),
340
+ queries: Object.freeze([...queries]),
341
+ version: cls.version,
342
+ declared: Object.freeze([...declared]),
343
+ });
344
+ }
345
+ function checkVersion(version, fail) {
346
+ if (version !== undefined && (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1)) {
347
+ fail(`declares version ${String(version)}; a version is a whole number of 1 or more`);
348
+ }
349
+ }
350
+ function checkDeclared(declared, fail) {
351
+ if (declared === undefined)
352
+ return [];
353
+ if (!Array.isArray(declared) || declared.some((q) => typeof q?.name !== "string" || q.name.trim() === "" || typeof q?.statement !== "string" || q.statement.trim() === "")) {
354
+ fail("declared must be a list of declaredQuery(name, statement)");
355
+ return [];
356
+ }
357
+ const names = declared.map((q) => q.name);
358
+ const twice = names.filter((n, i) => names.indexOf(n) !== i);
359
+ if (twice.length > 0)
360
+ fail(`declares the query '${twice[0]}' twice; each query has a name of its own`);
361
+ return declared;
362
+ }
363
+ /**
364
+ * A keyed view: one or more entities, each through a handler of its own. A topic is not a keyed view's
365
+ * source, and one entity is read once.
366
+ */
367
+ function registerKeyedView(cls, problems) {
368
+ const { fail, ok } = checker(cls, problems);
369
+ requireId(cls, fail);
370
+ requireShape(cls.row, "row: a schema or codec for its rows", fail);
371
+ checkVersion(cls.version, fail);
372
+ const declared = checkDeclared(cls.declared, fail);
373
+ const sources = new Map();
374
+ const declaredSources = cls.sources;
375
+ if (!Array.isArray(declaredSources) || declaredSources.length === 0) {
376
+ fail("declares no source; a keyed view reads one or more entities, each declared with on(...)");
377
+ }
378
+ else {
379
+ for (const [i, s] of declaredSources.entries()) {
380
+ const ref = s?.source;
381
+ const kind = ref?.prototype?._kind;
382
+ if (typeof s?.onChange !== "function" || typeof ref?.componentId !== "string" || typeof kind !== "string") {
383
+ fail(`sources[${i}] is not a source: declare it with on(EntityClass, events, handler)`);
384
+ continue;
385
+ }
386
+ if (kind !== "event-sourced" && kind !== "key-value") {
387
+ fail(`reads ${kind} ${JSON.stringify(ref.componentId)}; a keyed view reads event sourced and key value entities`);
388
+ continue;
389
+ }
390
+ if (!isShape(s.events)) {
391
+ fail(`sources[${i}] needs a schema or codec for ${ref.componentId}'s changes`);
392
+ continue;
393
+ }
394
+ if (sources.has(ref.componentId)) {
395
+ fail(`reads ${JSON.stringify(ref.componentId)} twice; each source is read once`);
396
+ continue;
397
+ }
398
+ sources.set(ref.componentId, Object.freeze({ id: ref.componentId, kind, eventCodec: codecFor(s.events), onChange: s.onChange, deleted: s.deleted }));
399
+ }
400
+ }
401
+ if (!ok())
402
+ return undefined;
403
+ return Object.freeze({
404
+ kind: "keyed-view",
405
+ id: cls.componentId,
406
+ cls,
407
+ sources,
408
+ rowCodec: codecFor(cls.row),
409
+ version: cls.version,
410
+ declared: Object.freeze([...declared]),
411
+ });
260
412
  }
261
413
  function registerConsumer(cls, problems) {
262
414
  const { fail, ok } = checker(cls, problems);
263
415
  requireId(cls, fail);
264
- const source = sourceOf(cls, fail);
416
+ const source = sourceOf(cls, fail, true);
265
417
  requireShape(cls.message, "message: a schema or codec for the source's messages", fail);
266
- if (cls.producesTo !== undefined && (typeof cls.producesTo !== "string" || cls.producesTo.trim() === ""))
267
- fail("producesTo must be a topic name");
268
- if (cls.producesTo !== undefined && !isShape(cls.out))
269
- fail(`produces to ${JSON.stringify(cls.producesTo)} and needs a static out: the shape of what it produces`);
418
+ const produces = publicationOf(cls.producesTo, fail);
419
+ if (produces !== undefined && !isShape(cls.out))
420
+ fail(`produces to ${JSON.stringify(produces.topic)} and needs a static out: the shape of what it produces`);
270
421
  if (!ok() || !source)
271
422
  return undefined;
272
423
  return Object.freeze({
@@ -276,15 +427,50 @@ function registerConsumer(cls, problems) {
276
427
  source,
277
428
  messageCodec: codecFor(cls.message),
278
429
  outCodec: cls.out ? codecFor(cls.out) : undefined,
279
- producesTo: cls.producesTo,
430
+ producesTo: produces?.topic,
431
+ produces,
280
432
  graph: false,
433
+ version: cls.version,
434
+ mirroredChanges: cls.mirroredChanges === true,
281
435
  });
282
436
  }
437
+ /**
438
+ * `producesTo` as declared: a topic name, or a Publication naming the topic with the contract stated
439
+ * for it and its broker. Undefined when the consumer produces nothing.
440
+ */
441
+ function publicationOf(declared, fail) {
442
+ if (declared === undefined)
443
+ return undefined;
444
+ if (typeof declared === "string") {
445
+ if (declared.trim() === "")
446
+ fail("producesTo must be a topic name");
447
+ return declared.trim() === "" ? undefined : { topic: declared };
448
+ }
449
+ const p = declared;
450
+ if (typeof p !== "object" || p === null || typeof p.topic !== "string" || p.topic.trim() === "") {
451
+ fail("producesTo must be a topic name, or { topic, contract?, broker? }");
452
+ return undefined;
453
+ }
454
+ if (p.contract !== undefined && !(p.contract instanceof Contract))
455
+ fail("producesTo.contract must be a Contract: Contract.fromFile(path, name)");
456
+ if (p.broker !== undefined && (typeof p.broker !== "string" || p.broker.trim() === ""))
457
+ fail("producesTo.broker must be the name of a broker the project declares");
458
+ if (p.project !== undefined && (typeof p.project !== "string" || p.project.trim() === ""))
459
+ fail("producesTo.project must be the id of the project whose topic this is");
460
+ if (p.project !== undefined && p.broker !== undefined)
461
+ fail("producesTo names a project and a broker; another project's topic is on the installation's broker");
462
+ return {
463
+ topic: p.topic,
464
+ ...(p.contract instanceof Contract ? { contract: p.contract } : {}),
465
+ ...(typeof p.broker === "string" ? { broker: p.broker } : {}),
466
+ ...(typeof p.project === "string" ? { project: p.project } : {}),
467
+ };
468
+ }
283
469
  /** A graph consumer is a consumer to discovery and to the runtime: one that produces deltas to its topic. */
284
470
  function registerGraphConsumer(cls, problems) {
285
471
  const { fail, ok } = checker(cls, problems);
286
472
  requireId(cls, fail);
287
- const source = sourceOf(cls, fail);
473
+ const source = sourceOf(cls, fail, true);
288
474
  requireShape(cls.message, "message: a schema or codec for the source's messages", fail);
289
475
  if (typeof cls.producesTo !== "string" || cls.producesTo.trim() === "")
290
476
  fail("needs a static producesTo: the topic its deltas are published to");
@@ -300,7 +486,10 @@ function registerGraphConsumer(cls, problems) {
300
486
  messageCodec: codecFor(cls.message),
301
487
  outCodec: graphDeltaCodec,
302
488
  producesTo: cls.producesTo,
489
+ produces: undefined,
303
490
  graph: true,
491
+ version: cls.version,
492
+ mirroredChanges: cls.mirroredChanges === true,
304
493
  });
305
494
  }
306
495
  function registerTimedAction(cls, problems) {
@@ -340,9 +529,10 @@ function registerAgent(cls, problems) {
340
529
  const maxToolCallSteps = cls.maxToolCallSteps ?? 100;
341
530
  if (!Number.isInteger(maxToolCallSteps) || maxToolCallSteps <= 0)
342
531
  fail("maxToolCallSteps must be a positive integer");
532
+ const { mcpServers, resultGuardrails } = collectMcp(cls, tools, fail);
343
533
  if (!ok())
344
534
  return undefined;
345
- return Object.freeze({ kind: "agent", id: cls.componentId, cls, handlers, tools, guardrails, role: cls.role ?? "", maxToolCallSteps });
535
+ return Object.freeze({ kind: "agent", id: cls.componentId, cls, handlers, tools, guardrails, mcpServers, resultGuardrails, role: cls.role ?? "", maxToolCallSteps });
346
536
  }
347
537
  function registerAutonomousAgent(cls, problems) {
348
538
  const { fail, ok } = checker(cls, problems);
@@ -378,9 +568,39 @@ function registerAutonomousAgent(cls, problems) {
378
568
  fail(`task type ${JSON.stringify(a.taskType.name)} needs a budget of at least one iteration`);
379
569
  taskTypes.set(a.taskType.name, a.taskType);
380
570
  }
571
+ const { mcpServers, resultGuardrails } = collectMcp(cls, tools, fail);
381
572
  if (!ok())
382
573
  return undefined;
383
- return Object.freeze({ kind: "autonomous-agent", id: cls.componentId, cls, tools, guardrails, taskTypes });
574
+ return Object.freeze({ kind: "autonomous-agent", id: cls.componentId, cls, tools, guardrails, mcpServers, resultGuardrails, taskTypes });
575
+ }
576
+ /** An agent's MCP servers and result guardrails by name, with what is wrong with them in the sidecar's words. */
577
+ function collectMcp(cls, tools, fail) {
578
+ const mcpServers = new Map();
579
+ for (const [property, server] of Object.entries(cls.mcpServers ?? {})) {
580
+ if (typeof server !== "object" || server === null || typeof server.name !== "string") {
581
+ fail(`mcpServers.${property} is not an MCP server: declare it with mcpServer(name, options)`);
582
+ continue;
583
+ }
584
+ serverProblems(server).forEach(fail);
585
+ if (mcpServers.has(server.name))
586
+ fail(`MCP server '${server.name}' is listed twice`);
587
+ mcpServers.set(server.name, server);
588
+ }
589
+ for (const t of tools.values()) {
590
+ if (t.name.startsWith(TOOL_PREFIX))
591
+ fail(`tool '${t.name}' takes the prefix '${TOOL_PREFIX}', which names an MCP server's tools`);
592
+ }
593
+ const resultGuardrails = new Map();
594
+ for (const [property, g] of Object.entries(cls.resultGuardrails ?? {})) {
595
+ if (typeof g !== "object" || g === null || typeof g.name !== "string" || typeof g.check !== "function") {
596
+ fail(`resultGuardrails.${property} is not a result guardrail: declare it with resultGuardrail(name, check)`);
597
+ continue;
598
+ }
599
+ if (resultGuardrails.has(g.name))
600
+ fail(`result guardrail '${g.name}' is declared twice`);
601
+ resultGuardrails.set(g.name, g);
602
+ }
603
+ return { mcpServers, resultGuardrails };
384
604
  }
385
605
  /** Collects a handler table by wire name, reporting a duplicate wire name or a kind the component cannot host. */
386
606
  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 {};