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.
- 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/consumer_pb.d.ts +52 -1
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +11 -1
- 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 +40 -2
- package/dist/consumer.js +50 -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/effects/stateless.d.ts +21 -0
- package/dist/effects/stateless.js +19 -0
- package/dist/endpoint.d.ts +10 -1
- package/dist/endpoint.js +20 -0
- package/dist/graph.d.ts +171 -0
- package/dist/graph.js +446 -0
- package/dist/handlers.d.ts +10 -2
- package/dist/handlers.js +6 -2
- package/dist/index.d.ts +15 -6
- package/dist/index.js +13 -4
- 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 +104 -16
- package/dist/server/workflow.js +5 -1
- package/dist/service.d.ts +45 -3
- package/dist/service.js +234 -12
- 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 +75 -5
- package/dist/testkit/kinds.js +264 -34
- 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/server/stateless.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/server/workflow.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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({
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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) {
|