ankka 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +35 -0
  2. package/dist/_proto/ankka/protocol/v1/agent_pb.js +8 -1
  3. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +542 -2
  4. package/dist/_proto/ankka/protocol/v1/client_pb.js +107 -6
  5. package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +52 -1
  6. package/dist/_proto/ankka/protocol/v1/consumer_pb.js +11 -1
  7. package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +330 -0
  8. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +81 -16
  9. package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +184 -0
  10. package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +26 -1
  11. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +63 -0
  12. package/dist/_proto/ankka/protocol/v1/view_pb.js +11 -1
  13. package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +7 -0
  14. package/dist/_proto/ankka/protocol/v1/workflow_pb.js +1 -1
  15. package/dist/agent.d.ts +15 -1
  16. package/dist/agent.js +20 -0
  17. package/dist/approvals.d.ts +47 -0
  18. package/dist/approvals.js +57 -0
  19. package/dist/autonomous.d.ts +23 -1
  20. package/dist/autonomous.js +30 -1
  21. package/dist/client.d.ts +92 -4
  22. package/dist/client.js +262 -15
  23. package/dist/consumer.d.ts +40 -2
  24. package/dist/consumer.js +50 -0
  25. package/dist/context.d.ts +4 -0
  26. package/dist/contract.d.ts +36 -0
  27. package/dist/contract.js +58 -0
  28. package/dist/effects/keyed.d.ts +33 -0
  29. package/dist/effects/keyed.js +41 -0
  30. package/dist/effects/stateless.d.ts +21 -0
  31. package/dist/effects/stateless.js +19 -0
  32. package/dist/endpoint.d.ts +10 -1
  33. package/dist/endpoint.js +20 -0
  34. package/dist/graph.d.ts +171 -0
  35. package/dist/graph.js +446 -0
  36. package/dist/handlers.d.ts +10 -2
  37. package/dist/handlers.js +6 -2
  38. package/dist/index.d.ts +15 -6
  39. package/dist/index.js +13 -4
  40. package/dist/keyedView.d.ts +56 -0
  41. package/dist/keyedView.js +67 -0
  42. package/dist/mcp.d.ts +57 -0
  43. package/dist/mcp.js +64 -0
  44. package/dist/routes.d.ts +23 -3
  45. package/dist/routes.js +16 -3
  46. package/dist/server/agent.js +12 -4
  47. package/dist/server/discovery.d.ts +12 -1
  48. package/dist/server/discovery.js +95 -1
  49. package/dist/server/http.d.ts +2 -1
  50. package/dist/server/http.js +78 -4
  51. package/dist/server/stateless.js +104 -16
  52. package/dist/server/workflow.js +5 -1
  53. package/dist/service.d.ts +45 -3
  54. package/dist/service.js +234 -12
  55. package/dist/services.d.ts +137 -0
  56. package/dist/services.js +259 -0
  57. package/dist/socket.d.ts +15 -0
  58. package/dist/socket.js +25 -0
  59. package/dist/spec.d.ts +1 -1
  60. package/dist/spec.js +87 -14
  61. package/dist/startFrom.d.ts +32 -0
  62. package/dist/startFrom.js +44 -0
  63. package/dist/testkit/index.d.ts +2 -2
  64. package/dist/testkit/index.js +1 -1
  65. package/dist/testkit/integration.js +5 -0
  66. package/dist/testkit/kinds.d.ts +75 -5
  67. package/dist/testkit/kinds.js +264 -34
  68. package/dist/testkit/unit.d.ts +14 -1
  69. package/dist/testkit/unit.js +40 -3
  70. package/dist/timedAction.d.ts +17 -2
  71. package/dist/timedAction.js +33 -1
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/dist/view.d.ts +32 -0
  75. package/dist/view.js +8 -0
  76. package/dist/workflow.d.ts +18 -1
  77. package/dist/workflow.js +39 -0
  78. package/package.json +3 -2
@@ -8,12 +8,54 @@ import { TimedAction as TimedActionService, TimedActionEffectSchema } from "../_
8
8
  import { codecFor } from "../codec.js";
9
9
  import { metadataFromProto, metadataToProto } from "../context.js";
10
10
  import { ErrorCode } from "../effects/common.js";
11
+ import { clientRows } from "../keyedView.js";
12
+ import { ProtocolVersionError, requireSeveralMessages } from "../consumer.js";
13
+ import { GRAPH_DELTA_SCHEMA, deltaRecords } from "../graph.js";
14
+ import { JSON_CONTENT } from "../codec.js";
11
15
  import { errorCodeToProto } from "../kinds.js";
12
16
  import { decodePayload, encodePayload } from "./payloads.js";
17
+ import { PayloadSchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
13
18
  function messageOf(e) {
14
19
  return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
15
20
  }
21
+ /**
22
+ * A keyed view's change: sent with the component it came from and no row, answered with the rows the
23
+ * source's handler names. The view reads its own rows through the client, on its own id.
24
+ */
25
+ async function handleKeyedView(req, sourceId, ctx) {
26
+ const registered = ctx.registry.of("keyed-view", req.componentId);
27
+ if (!registered)
28
+ throw new ConnectError(`no keyed view ${JSON.stringify(req.componentId)} is registered`, Code.NotFound);
29
+ const source = registered.sources.get(sourceId);
30
+ if (!source)
31
+ throw new ConnectError(`keyed view ${registered.id} reads no ${JSON.stringify(sourceId)}`, Code.NotFound);
32
+ const metadata = metadataFromProto(req.metadata);
33
+ const view = new registered.cls();
34
+ try {
35
+ const client = ctx.client.withMetadata(metadata);
36
+ view._bind(metadata, client, clientRows(client, registered.id, registered.rowCodec));
37
+ const effect = (req.deleted
38
+ ? source.deleted
39
+ ? await source.deleted(view)
40
+ : undefined
41
+ : await source.onChange(view, decodePayload(source.eventCodec, req.event)));
42
+ if (effect !== undefined && effect?.kind !== "rows")
43
+ throw new TypeError(`${registered.id}'s handler for ${sourceId} returned something that is not a keyed view effect`);
44
+ const changes = (effect?.changes ?? []).map((c) => "deleted" in c
45
+ ? { key: c.key, change: { case: "delete", value: {} } }
46
+ : { key: c.key, change: { case: "upsert", value: encodePayload(registered.rowCodec, c.row) } });
47
+ return create(ViewEffectSchema, { effect: { case: "rows", value: { changes } } });
48
+ }
49
+ catch (e) {
50
+ if (e instanceof ConnectError)
51
+ throw e;
52
+ ctx.log(`ankka: keyed view ${registered.id} on ${sourceId} ${metadata["ce-subject"] ?? "?"} threw: ${messageOf(e)}`);
53
+ throw new ConnectError(messageOf(e), Code.Internal);
54
+ }
55
+ }
16
56
  export async function handleView(req, ctx) {
57
+ if (req.sourceId !== undefined)
58
+ return handleKeyedView(req, req.sourceId, ctx);
17
59
  const registered = ctx.registry.of("view", req.componentId);
18
60
  if (!registered)
19
61
  throw new ConnectError(`no view ${JSON.stringify(req.componentId)} is registered`, Code.NotFound);
@@ -41,33 +83,79 @@ export async function handleView(req, ctx) {
41
83
  throw new ConnectError(messageOf(e), Code.Internal);
42
84
  }
43
85
  }
86
+ const DONE = () => create(ConsumerEffectSchema, { effect: { case: "done", value: {} } });
87
+ const IGNORE = () => create(ConsumerEffectSchema, { effect: { case: "ignore", value: {} } });
88
+ /**
89
+ * Several messages as the reply `produce_all`, which only a runtime at protocol 1.3 understands: the
90
+ * request must have said so. None at all is `done`, which every runtime understands and means the same.
91
+ */
92
+ function produceAll(request, messages) {
93
+ if (messages.length === 0)
94
+ return DONE();
95
+ requireSeveralMessages(request);
96
+ return create(ConsumerEffectSchema, {
97
+ effect: {
98
+ case: "produceAll",
99
+ value: { messages: messages.map((m) => ({ payload: m.payload, metadata: metadataToProto(m.metadata), ...(m.key !== undefined ? { key: m.key } : {}) })) },
100
+ },
101
+ });
102
+ }
103
+ async function graphEffect(registered, req, metadata, ctx) {
104
+ const consumer = new registered.cls();
105
+ consumer._bind(metadata, ctx.client.withMetadata(metadata));
106
+ const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
107
+ switch (effect.kind) {
108
+ case "publish":
109
+ return produceAll(metadata, deltaRecords(effect, metadata).map((r) => ({
110
+ payload: create(PayloadSchema, { contentType: JSON_CONTENT, manifest: GRAPH_DELTA_SCHEMA, data: r.value }),
111
+ metadata: { "ce-type": GRAPH_DELTA_SCHEMA },
112
+ key: r.key,
113
+ })));
114
+ case "done":
115
+ return DONE();
116
+ case "ignore":
117
+ return IGNORE();
118
+ default:
119
+ throw new TypeError(`${registered.id}.onMessage returned something that is not a graph effect`);
120
+ }
121
+ }
122
+ async function consumerEffect(registered, req, metadata, ctx) {
123
+ const consumer = new registered.cls();
124
+ consumer._bind(metadata, ctx.client.withMetadata(metadata));
125
+ const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
126
+ const outCodec = () => {
127
+ if (!registered.outCodec)
128
+ throw new Error(`${registered.id} produced a message but declares no out shape`);
129
+ return registered.outCodec;
130
+ };
131
+ switch (effect.kind) {
132
+ case "produce":
133
+ return create(ConsumerEffectSchema, { effect: { case: "produce", value: { payload: encodePayload(outCodec(), effect.payload), metadata: metadataToProto(effect.metadata) } } });
134
+ case "produceAll":
135
+ return produceAll(metadata, effect.messages.map((m) => ({ payload: encodePayload(outCodec(), m.payload), metadata: m.metadata ?? {}, ...(m.key !== undefined ? { key: m.key } : {}) })));
136
+ case "done":
137
+ return DONE();
138
+ case "ignore":
139
+ return IGNORE();
140
+ default:
141
+ throw new TypeError(`${registered.id}.onMessage returned something that is not a consumer effect`);
142
+ }
143
+ }
44
144
  export async function handleConsumer(req, ctx) {
45
145
  const registered = ctx.registry.of("consumer", req.componentId);
46
146
  if (!registered)
47
147
  throw new ConnectError(`no consumer ${JSON.stringify(req.componentId)} is registered`, Code.NotFound);
48
148
  const metadata = metadataFromProto(req.metadata);
49
- const consumer = new registered.cls();
50
149
  try {
51
- consumer._bind(metadata, ctx.client.withMetadata(metadata));
52
- const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
53
- switch (effect.kind) {
54
- case "produce": {
55
- if (!registered.outCodec)
56
- throw new Error(`${registered.id} produced a message but declares no out shape`);
57
- return create(ConsumerEffectSchema, { effect: { case: "produce", value: { payload: encodePayload(registered.outCodec, effect.payload), metadata: metadataToProto(effect.metadata) } } });
58
- }
59
- case "done":
60
- return create(ConsumerEffectSchema, { effect: { case: "done", value: {} } });
61
- case "ignore":
62
- return create(ConsumerEffectSchema, { effect: { case: "ignore", value: {} } });
63
- default:
64
- throw new TypeError(`${registered.id}.onMessage returned something that is not a consumer effect`);
65
- }
150
+ return registered.graph ? await graphEffect(registered, req, metadata, ctx) : await consumerEffect(registered, req, metadata, ctx);
66
151
  }
67
152
  catch (e) {
68
153
  if (e instanceof ConnectError)
69
154
  throw e;
70
155
  ctx.log(`ankka: consumer ${registered.id} on ${metadata["ce-subject"] ?? "?"} threw: ${messageOf(e)}`);
156
+ // The message alone, so the runtime's log says what the SDK needs and of which runtime.
157
+ if (e instanceof ProtocolVersionError)
158
+ throw new ConnectError(e.message, Code.Internal);
71
159
  throw new ConnectError(messageOf(e), Code.Internal);
72
160
  }
73
161
  }
@@ -114,7 +114,11 @@ class WorkflowStream {
114
114
  return failure(run.id, { message: `${registered.id}/${run.step}: the input could not be decoded: ${messageOf(e)}`, code: ErrorCode.BadRequest });
115
115
  }
116
116
  let effect;
117
- fresh._bindCommand(this.state, commandContext(registered.id, this.entityId, 0n, {}), ctx.client);
117
+ // What the sidecar says about this step: its trace, and the step as the caller of whatever it calls.
118
+ // Carried on the step's calls as a command's is; a sidecar before 1.3 sends none.
119
+ const metadata = metadataFromProto(run.metadata);
120
+ fresh._bindCommand(this.state, commandContext(registered.id, this.entityId, 0n, metadata), ctx.client.withMetadata(metadata));
121
+ fresh._enterStep();
118
122
  try {
119
123
  effect = (await step.run(fresh, input));
120
124
  }
package/dist/service.d.ts CHANGED
@@ -1,11 +1,16 @@
1
+ import { type StartFrom } from "./startFrom.ts";
2
+ import { Contract, type Publication } from "./contract.ts";
1
3
  import { type Codec } from "./codec.ts";
2
4
  import { EventSourcedEntity, type EventSourcedEntityClass } from "./eventSourcedEntity.ts";
3
5
  import { KeyValueEntity, type KeyValueEntityClass } from "./keyValueEntity.ts";
4
6
  import { Workflow, type WorkflowClass } from "./workflow.ts";
5
- import { View, type ViewClass } from "./view.ts";
7
+ import { View, type DeclaredQuery, type ViewClass } from "./view.ts";
8
+ import { KeyedView, type KeyedViewClass } from "./keyedView.ts";
6
9
  import { Consumer, type ConsumerClass } from "./consumer.ts";
10
+ import { GraphConsumer, type GraphConsumerClass } from "./graph.ts";
7
11
  import { TimedAction, type TimedActionClass } from "./timedAction.ts";
8
12
  import { Agent, type AgentClass } from "./agent.ts";
13
+ import { type McpServerRef, type ResultGuardrailRef } from "./mcp.ts";
9
14
  import { AutonomousAgent, type AutonomousAgentClass, type TaskType } from "./autonomous.ts";
10
15
  import { Endpoint, type EndpointClass } from "./endpoint.ts";
11
16
  import type { GuardrailRef, HandlerRef, ToolRef } from "./handlers.ts";
@@ -48,6 +53,13 @@ export type Source = {
48
53
  };
49
54
  } | {
50
55
  readonly topic: string;
56
+ readonly startFrom?: StartFrom;
57
+ /** The contract the topic is expected to carry. */
58
+ readonly contract?: Contract;
59
+ /** The declared broker the topic is on; absent is the installation's. */
60
+ readonly broker?: string;
61
+ /** Partitions handled at once, each in order. */
62
+ readonly parallel?: boolean;
51
63
  };
52
64
  export interface RegisteredView {
53
65
  readonly kind: "view";
@@ -57,15 +69,39 @@ export interface RegisteredView {
57
69
  readonly eventCodec: Codec<any>;
58
70
  readonly rowCodec: Codec<any>;
59
71
  readonly queries: readonly string[];
72
+ readonly version: number | undefined;
73
+ readonly declared: readonly DeclaredQuery[];
74
+ }
75
+ /** One source of a keyed view: the entity it reads, how its changes decode, and its handlers. */
76
+ export interface RegisteredKeyedSource {
77
+ readonly id: string;
78
+ readonly kind: ComponentKind;
79
+ readonly eventCodec: Codec<any>;
80
+ readonly onChange: (view: any, event: any) => unknown;
81
+ readonly deleted: ((view: any) => unknown) | undefined;
82
+ }
83
+ export interface RegisteredKeyedView {
84
+ readonly kind: "keyed-view";
85
+ readonly id: string;
86
+ readonly cls: KeyedViewClass<any, any>;
87
+ readonly sources: ReadonlyMap<string, RegisteredKeyedSource>;
88
+ readonly rowCodec: Codec<any>;
89
+ readonly version: number | undefined;
90
+ readonly declared: readonly DeclaredQuery[];
60
91
  }
61
92
  export interface RegisteredConsumer {
62
93
  readonly kind: "consumer";
63
94
  readonly id: string;
64
- readonly cls: ConsumerClass<any, any, any>;
95
+ readonly cls: ConsumerClass<any, any, any> | GraphConsumerClass<any, any>;
65
96
  readonly source: Source;
66
97
  readonly messageCodec: Codec<any>;
67
98
  readonly outCodec: Codec<any> | undefined;
68
99
  readonly producesTo: string | undefined;
100
+ /** The publication as declared: the topic with the contract stated for it and its broker. */
101
+ readonly produces: Publication | undefined;
102
+ /** A graph consumer: its handlers return elements, each published as a delta under its element key. */
103
+ readonly graph: boolean;
104
+ readonly version: number | undefined;
69
105
  }
70
106
  export interface RegisteredTimedAction {
71
107
  readonly kind: "timed-action";
@@ -80,6 +116,8 @@ export interface RegisteredAgent {
80
116
  readonly handlers: ReadonlyMap<string, HandlerRef<any, any, any, any>>;
81
117
  readonly tools: ReadonlyMap<string, ToolRef<any, any>>;
82
118
  readonly guardrails: ReadonlyMap<string, GuardrailRef>;
119
+ readonly mcpServers: ReadonlyMap<string, McpServerRef>;
120
+ readonly resultGuardrails: ReadonlyMap<string, ResultGuardrailRef>;
83
121
  readonly role: string;
84
122
  readonly maxToolCallSteps: number;
85
123
  }
@@ -89,9 +127,11 @@ export interface RegisteredAutonomousAgent {
89
127
  readonly cls: AutonomousAgentClass<any>;
90
128
  readonly tools: ReadonlyMap<string, ToolRef<any, any>>;
91
129
  readonly guardrails: ReadonlyMap<string, GuardrailRef>;
130
+ readonly mcpServers: ReadonlyMap<string, McpServerRef>;
131
+ readonly resultGuardrails: ReadonlyMap<string, ResultGuardrailRef>;
92
132
  readonly taskTypes: ReadonlyMap<string, TaskType<any>>;
93
133
  }
94
- export type RegisteredComponent = RegisteredEventSourced | RegisteredKeyValue | RegisteredWorkflow | RegisteredView | RegisteredConsumer | RegisteredTimedAction | RegisteredAgent | RegisteredAutonomousAgent;
134
+ export type RegisteredComponent = RegisteredEventSourced | RegisteredKeyValue | RegisteredWorkflow | RegisteredView | RegisteredKeyedView | RegisteredConsumer | RegisteredTimedAction | RegisteredAgent | RegisteredAutonomousAgent;
95
135
  export interface RegisteredEndpoint {
96
136
  readonly id: string;
97
137
  readonly cls: EndpointClass<any>;
@@ -133,7 +173,9 @@ export declare class ServiceBuilder {
133
173
  register<S, C extends KeyValueEntity<S>>(cls: KeyValueEntityClass<S, C>): this;
134
174
  register<S, C extends Workflow<S>>(cls: WorkflowClass<S, C>): this;
135
175
  register<E, Row, C extends View<E, Row>>(cls: ViewClass<E, Row, C>): this;
176
+ register<Row, C extends KeyedView<Row>>(cls: KeyedViewClass<Row, C>): this;
136
177
  register<M, Out, C extends Consumer<M, Out>>(cls: ConsumerClass<M, Out, C>): this;
178
+ register<M, C extends GraphConsumer<M>>(cls: GraphConsumerClass<M, C>): this;
137
179
  register<C extends TimedAction>(cls: TimedActionClass<C>): this;
138
180
  register<C extends Agent>(cls: AgentClass<C>): this;
139
181
  register<C extends AutonomousAgent>(cls: AutonomousAgentClass<C>): this;
package/dist/service.js CHANGED
@@ -3,14 +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";
15
+ import { GraphConsumer, graphDeltaCodec } from "./graph.js";
12
16
  import { TimedAction } from "./timedAction.js";
13
17
  import { Agent } from "./agent.js";
18
+ import { serverProblems, TOOL_PREFIX } from "./mcp.js";
14
19
  import { AutonomousAgent, COMPLETE_TASK, FAIL_TASK } from "./autonomous.js";
15
20
  import { Endpoint } from "./endpoint.js";
16
21
  import { isAcl } from "./routes.js";
@@ -95,8 +100,12 @@ export class ServiceBuilder {
95
100
  add(registerWorkflow(cls, problems), cls);
96
101
  else if (extendsBase(cls, View))
97
102
  add(registerView(cls, problems), cls);
103
+ else if (extendsBase(cls, KeyedView))
104
+ add(registerKeyedView(cls, problems), cls);
98
105
  else if (extendsBase(cls, Consumer))
99
106
  add(registerConsumer(cls, problems), cls);
107
+ else if (extendsBase(cls, GraphConsumer))
108
+ add(registerGraphConsumer(cls, problems), cls);
100
109
  else if (extendsBase(cls, TimedAction))
101
110
  add(registerTimedAction(cls, problems), cls);
102
111
  else if (extendsBase(cls, Agent))
@@ -120,7 +129,7 @@ export class ServiceBuilder {
120
129
  }
121
130
  }
122
131
  else {
123
- problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, Consumer, 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`);
124
133
  }
125
134
  }
126
135
  if (problems.length > 0)
@@ -220,19 +229,59 @@ function registerWorkflow(cls, problems) {
220
229
  return undefined;
221
230
  return Object.freeze({ kind: "workflow", id: cls.componentId, cls, stateCodec: codecFor(cls.state), handlers, steps, settings });
222
231
  }
223
- function sourceOf(cls, fail) {
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) {
224
266
  const hasSource = cls.source !== undefined;
225
267
  const hasTopic = cls.topic !== undefined;
226
268
  if (hasSource === hasTopic) {
227
269
  fail("needs a static source (a component class) or a static topic, and not both");
228
270
  return undefined;
229
271
  }
272
+ topicProblems(cls, consumer, fail);
230
273
  if (hasTopic) {
231
274
  if (typeof cls.topic !== "string" || cls.topic.trim() === "") {
232
275
  fail("topic must be a non-empty string");
233
276
  return undefined;
234
277
  }
235
- return { topic: cls.topic };
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
+ };
236
285
  }
237
286
  const src = cls.source;
238
287
  const kind = src?.prototype?._kind;
@@ -245,25 +294,112 @@ function sourceOf(cls, fail) {
245
294
  function registerView(cls, problems) {
246
295
  const { fail, ok } = checker(cls, problems);
247
296
  requireId(cls, fail);
248
- const source = sourceOf(cls, fail);
297
+ const source = sourceOf(cls, fail, false);
249
298
  requireShape(cls.events, "events: a schema or codec for the source's events", fail);
250
299
  requireShape(cls.row, "row: a schema or codec for its rows", fail);
251
300
  const queries = cls.queries ?? ["get", "all"];
252
301
  if (!Array.isArray(queries) || queries.some((q) => typeof q !== "string" || q.trim() === ""))
253
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
+ }
254
313
  if (!ok() || !source)
255
314
  return undefined;
256
- return Object.freeze({ kind: "view", id: cls.componentId, cls, source, eventCodec: codecFor(cls.events), rowCodec: codecFor(cls.row), queries: Object.freeze([...queries]) });
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
+ });
257
394
  }
258
395
  function registerConsumer(cls, problems) {
259
396
  const { fail, ok } = checker(cls, problems);
260
397
  requireId(cls, fail);
261
- const source = sourceOf(cls, fail);
398
+ const source = sourceOf(cls, fail, true);
262
399
  requireShape(cls.message, "message: a schema or codec for the source's messages", fail);
263
- if (cls.producesTo !== undefined && (typeof cls.producesTo !== "string" || cls.producesTo.trim() === ""))
264
- fail("producesTo must be a topic name");
265
- if (cls.producesTo !== undefined && !isShape(cls.out))
266
- 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`);
267
403
  if (!ok() || !source)
268
404
  return undefined;
269
405
  return Object.freeze({
@@ -273,7 +409,62 @@ function registerConsumer(cls, problems) {
273
409
  source,
274
410
  messageCodec: codecFor(cls.message),
275
411
  outCodec: cls.out ? codecFor(cls.out) : undefined,
412
+ producesTo: produces?.topic,
413
+ produces,
414
+ graph: false,
415
+ version: cls.version,
416
+ });
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
+ }
445
+ /** A graph consumer is a consumer to discovery and to the runtime: one that produces deltas to its topic. */
446
+ function registerGraphConsumer(cls, problems) {
447
+ const { fail, ok } = checker(cls, problems);
448
+ requireId(cls, fail);
449
+ const source = sourceOf(cls, fail, true);
450
+ requireShape(cls.message, "message: a schema or codec for the source's messages", fail);
451
+ if (typeof cls.producesTo !== "string" || cls.producesTo.trim() === "")
452
+ fail("needs a static producesTo: the topic its deltas are published to");
453
+ if (cls.out !== undefined)
454
+ fail("declares a static out, which a graph consumer does not have: what it produces is graph deltas");
455
+ if (!ok() || !source)
456
+ return undefined;
457
+ return Object.freeze({
458
+ kind: "consumer",
459
+ id: cls.componentId,
460
+ cls,
461
+ source,
462
+ messageCodec: codecFor(cls.message),
463
+ outCodec: graphDeltaCodec,
276
464
  producesTo: cls.producesTo,
465
+ produces: undefined,
466
+ graph: true,
467
+ version: cls.version,
277
468
  });
278
469
  }
279
470
  function registerTimedAction(cls, problems) {
@@ -313,9 +504,10 @@ function registerAgent(cls, problems) {
313
504
  const maxToolCallSteps = cls.maxToolCallSteps ?? 100;
314
505
  if (!Number.isInteger(maxToolCallSteps) || maxToolCallSteps <= 0)
315
506
  fail("maxToolCallSteps must be a positive integer");
507
+ const { mcpServers, resultGuardrails } = collectMcp(cls, tools, fail);
316
508
  if (!ok())
317
509
  return undefined;
318
- 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 });
319
511
  }
320
512
  function registerAutonomousAgent(cls, problems) {
321
513
  const { fail, ok } = checker(cls, problems);
@@ -351,9 +543,39 @@ function registerAutonomousAgent(cls, problems) {
351
543
  fail(`task type ${JSON.stringify(a.taskType.name)} needs a budget of at least one iteration`);
352
544
  taskTypes.set(a.taskType.name, a.taskType);
353
545
  }
546
+ const { mcpServers, resultGuardrails } = collectMcp(cls, tools, fail);
354
547
  if (!ok())
355
548
  return undefined;
356
- 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 };
357
579
  }
358
580
  /** Collects a handler table by wire name, reporting a duplicate wire name or a kind the component cannot host. */
359
581
  export function collectHandlers(table, what, allowed, fail) {