ankka 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) 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/discovery_pb.d.ts +330 -0
  6. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +81 -16
  7. package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +184 -0
  8. package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +26 -1
  9. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +63 -0
  10. package/dist/_proto/ankka/protocol/v1/view_pb.js +11 -1
  11. package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +7 -0
  12. package/dist/_proto/ankka/protocol/v1/workflow_pb.js +1 -1
  13. package/dist/agent.d.ts +15 -1
  14. package/dist/agent.js +20 -0
  15. package/dist/approvals.d.ts +47 -0
  16. package/dist/approvals.js +57 -0
  17. package/dist/autonomous.d.ts +23 -1
  18. package/dist/autonomous.js +30 -1
  19. package/dist/client.d.ts +92 -4
  20. package/dist/client.js +262 -15
  21. package/dist/consumer.d.ts +24 -2
  22. package/dist/consumer.js +20 -0
  23. package/dist/context.d.ts +4 -0
  24. package/dist/contract.d.ts +36 -0
  25. package/dist/contract.js +58 -0
  26. package/dist/effects/keyed.d.ts +33 -0
  27. package/dist/effects/keyed.js +41 -0
  28. package/dist/endpoint.d.ts +10 -1
  29. package/dist/endpoint.js +20 -0
  30. package/dist/graph.d.ts +15 -1
  31. package/dist/graph.js +20 -0
  32. package/dist/handlers.d.ts +10 -2
  33. package/dist/handlers.js +6 -2
  34. package/dist/index.d.ts +12 -4
  35. package/dist/index.js +11 -3
  36. package/dist/keyedView.d.ts +56 -0
  37. package/dist/keyedView.js +67 -0
  38. package/dist/mcp.d.ts +57 -0
  39. package/dist/mcp.js +64 -0
  40. package/dist/routes.d.ts +23 -3
  41. package/dist/routes.js +16 -3
  42. package/dist/server/agent.js +12 -4
  43. package/dist/server/discovery.d.ts +12 -1
  44. package/dist/server/discovery.js +95 -1
  45. package/dist/server/http.d.ts +2 -1
  46. package/dist/server/http.js +78 -4
  47. package/dist/server/stateless.js +38 -0
  48. package/dist/server/workflow.js +5 -1
  49. package/dist/service.d.ts +40 -2
  50. package/dist/service.js +209 -14
  51. package/dist/services.d.ts +137 -0
  52. package/dist/services.js +259 -0
  53. package/dist/socket.d.ts +15 -0
  54. package/dist/socket.js +25 -0
  55. package/dist/spec.d.ts +1 -1
  56. package/dist/spec.js +87 -14
  57. package/dist/startFrom.d.ts +32 -0
  58. package/dist/startFrom.js +44 -0
  59. package/dist/testkit/index.d.ts +2 -2
  60. package/dist/testkit/index.js +1 -1
  61. package/dist/testkit/integration.js +5 -0
  62. package/dist/testkit/kinds.d.ts +43 -2
  63. package/dist/testkit/kinds.js +196 -26
  64. package/dist/testkit/unit.d.ts +14 -1
  65. package/dist/testkit/unit.js +40 -3
  66. package/dist/timedAction.d.ts +17 -2
  67. package/dist/timedAction.js +33 -1
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/dist/view.d.ts +32 -0
  71. package/dist/view.js +8 -0
  72. package/dist/workflow.d.ts +18 -1
  73. package/dist/workflow.js +39 -0
  74. package/package.json +3 -2
package/dist/endpoint.js CHANGED
@@ -8,7 +8,9 @@
8
8
  // static readonly routes = { getCart: get("/{cartId}", ShoppingCart, (ep: ShoppingCartEndpoint, req) => ep.getCart(req.params.cartId)) }
9
9
  // getCart(cartId: string) { return this.client.of(ShoppingCartEntity, cartId).call(ShoppingCartEntity.handlers.getCart).invoke() }
10
10
  // }
11
+ import { secretsFor } from "./client.js";
11
12
  import { currentRequest, requestIfAny } from "./context.js";
13
+ import { servicesFor } from "./services.js";
12
14
  export class Endpoint {
13
15
  #client;
14
16
  /** The request being handled: path parameters, query, headers, principal. Request-scoped. */
@@ -22,6 +24,24 @@ export class Endpoint {
22
24
  const request = requestIfAny();
23
25
  return request ? this.#client.withMetadata(request.metadata) : this.#client;
24
26
  }
27
+ #secrets;
28
+ /** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
29
+ get secrets() {
30
+ return this.#secrets ?? secretsFor(this.client);
31
+ }
32
+ /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
33
+ set secrets(store) {
34
+ this.#secrets = store;
35
+ }
36
+ #services;
37
+ /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
38
+ get services() {
39
+ return this.#services ?? servicesFor(this.client);
40
+ }
41
+ /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
42
+ set services(services) {
43
+ this.#services = services;
44
+ }
25
45
  /** @internal */
26
46
  _bindClient(client) {
27
47
  this.#client = client;
package/dist/graph.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import type { StartFrom } from "./startFrom.ts";
1
2
  import { type Codec, type Shape } from "./codec.ts";
2
3
  import type { EffectLike, Metadata } from "./effects/common.ts";
3
- import type { ComponentClient, ComponentRef } from "./client.ts";
4
+ import { type ComponentClient, type ComponentRef, type Secrets } from "./client.ts";
5
+ import { type Services } from "./services.ts";
4
6
  type MaybePromise<T> = T | Promise<T>;
5
7
  /** The contract's name: the manifest and the `ce-type` of every delta. */
6
8
  export declare const GRAPH_DELTA_SCHEMA = "ankka.graph-delta.v1";
@@ -137,6 +139,14 @@ export declare abstract class GraphConsumer<M> {
137
139
  /** The change's sequence number, which is the version of every element that states none. */
138
140
  get sequenceNumber(): bigint | undefined;
139
141
  get client(): ComponentClient;
142
+ /** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
143
+ get secrets(): Secrets;
144
+ /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
145
+ set secrets(store: Secrets);
146
+ /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
147
+ get services(): Services;
148
+ /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
149
+ set services(services: Services);
140
150
  /** @internal */
141
151
  get _kind(): "consumer";
142
152
  abstract onMessage(message: M): MaybePromise<GraphEffect>;
@@ -150,6 +160,10 @@ export interface GraphConsumerClass<M = unknown, C extends GraphConsumer<M> = Gr
150
160
  readonly componentId: string;
151
161
  readonly source?: ComponentRef;
152
162
  readonly topic?: string;
163
+ /** Where a topic source starts the first time its group reads the topic. */
164
+ readonly startFrom?: StartFrom;
165
+ /** Raised to read the topic again from the start position, under a group of its own. Absent is 1. */
166
+ readonly version?: number;
153
167
  readonly message: Shape<M>;
154
168
  /** The delta topic. It must be compacted to hold the graph; the pipeline that reads it creates it so. */
155
169
  readonly producesTo: string;
package/dist/graph.js CHANGED
@@ -15,8 +15,10 @@
15
15
  // }
16
16
  // }
17
17
  import { JSON_CONTENT } from "./codec.js";
18
+ import { secretsFor } from "./client.js";
18
19
  import { sequenceNumberOf } from "./consumer.js";
19
20
  import { renderDouble, reviver } from "./json.js";
21
+ import { servicesFor } from "./services.js";
20
22
  /** The contract's name: the manifest and the `ce-type` of every delta. */
21
23
  export const GRAPH_DELTA_SCHEMA = "ankka.graph-delta.v1";
22
24
  /** The record key of every delta for the node `id`. */
@@ -410,6 +412,24 @@ export class GraphConsumer {
410
412
  throw new Error("client is only available inside onMessage or onDelete");
411
413
  return this.#client;
412
414
  }
415
+ #secrets;
416
+ /** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
417
+ get secrets() {
418
+ return this.#secrets ?? secretsFor(this.client);
419
+ }
420
+ /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
421
+ set secrets(store) {
422
+ this.#secrets = store;
423
+ }
424
+ #services;
425
+ /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
426
+ get services() {
427
+ return this.#services ?? servicesFor(this.client);
428
+ }
429
+ /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
430
+ set services(services) {
431
+ this.#services = services;
432
+ }
413
433
  /** @internal */
414
434
  get _kind() {
415
435
  return "consumer";
@@ -1,3 +1,4 @@
1
+ import { type Approval } from "./approvals.ts";
1
2
  import type { Shape } from "./codec.ts";
2
3
  import type { EffectLike, ReadOnlyLike } from "./effects/common.ts";
3
4
  export type HandlerKind = "command" | "query" | "stream" | "step" | "action";
@@ -35,14 +36,21 @@ export declare function step<C, E extends EffectLike>(name: string, run: (self:
35
36
  /** A timed action: `action("remind", s.string, (t, id) => t.remind(id))`. */
36
37
  export declare function action<C, I, E extends EffectLike>(name: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<E>): HandlerRef<C, I, undefined, E>;
37
38
  export declare function action<C, E extends EffectLike>(name: string, run: (self: C) => MaybePromise<E>): HandlerRef<C, undefined, undefined, E>;
38
- /** An agent tool: the model sees `description` and the JSON Schema of `input`; the process runs `run`. */
39
+ /**
40
+ * An agent tool: the model sees `description` and the JSON Schema of `input`; the process runs `run`.
41
+ * With `approval`, the tool waits for a person's decision before it runs: the caller is answered with an
42
+ * approval request, and the turn goes on when someone decides it.
43
+ */
39
44
  export interface ToolRef<C = unknown, I = unknown> {
40
45
  readonly name: string;
41
46
  readonly description: string;
42
47
  readonly input: Shape<I>;
43
48
  readonly run: (self: C, input: I) => MaybePromise<unknown>;
49
+ readonly approval?: Approval;
44
50
  }
45
- export declare function tool<C, I>(name: string, description: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<unknown>): ToolRef<C, I>;
51
+ export declare function tool<C, I>(name: string, description: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<unknown>, options?: {
52
+ readonly approval?: Approval;
53
+ }): ToolRef<C, I>;
46
54
  export type GuardrailStage = "input" | "output";
47
55
  /** An agent guardrail: returns a reason to block, or `null` to let the text through. */
48
56
  export interface GuardrailRef {
package/dist/handlers.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // and `guardrails` tables. Each carries the wire name, the input and reply schemas, and the function
3
3
  // that runs it. The wire name is declared here and nowhere else; renaming the method it calls changes
4
4
  // nothing on the wire. These are plain values, so the registry can read them without reflection.
5
+ import { approvalProblem } from "./approvals.js";
5
6
  function ref(kind, name, input, reply, run, flags) {
6
7
  if (typeof name !== "string" || name.trim() === "")
7
8
  throw new TypeError(`a ${kind} needs a wire name`);
@@ -49,13 +50,16 @@ export function action(name, ...rest) {
49
50
  const [run] = rest;
50
51
  return ref("action", name, undefined, undefined, run, { readOnly: false, streaming: false });
51
52
  }
52
- export function tool(name, description, input, run) {
53
+ export function tool(name, description, input, run, options = {}) {
53
54
  if (typeof name !== "string" || name.trim() === "")
54
55
  throw new TypeError("a tool needs a name");
55
56
  if (typeof description !== "string" || description.trim() === "") {
56
57
  throw new TypeError(`tool ${name}: a tool needs a description, or the model cannot choose it`);
57
58
  }
58
- return Object.freeze({ name, description, input, run });
59
+ const problem = approvalProblem(options.approval);
60
+ if (problem)
61
+ throw new TypeError(`tool ${name}: ${problem}`);
62
+ return Object.freeze({ name, description, input, run, ...(options.approval !== undefined ? { approval: options.approval } : {}) });
59
63
  }
60
64
  export function guardrail(name, check) {
61
65
  if (typeof name !== "string" || name.trim() === "")
package/dist/index.d.ts CHANGED
@@ -12,20 +12,28 @@ export { KeyValueEntity, type KeyValueEntityClass } from "./keyValueEntity.ts";
12
12
  export { WorkflowEffects, StepEffects, WorkflowUpdateBuilder, StepUpdateBuilder, workflowSettings, type WorkflowEffect, type WorkflowReadOnlyEffect, type WorkflowCommandEffect, type StepEffect, type StepOutcome, type StepRef, type PauseOptions, type WorkflowSettings, type StepSettings, type Recovery, } from "./effects/workflow.ts";
13
13
  export { Workflow, type WorkflowClass } from "./workflow.ts";
14
14
  export { ViewEffects, ConsumerEffects, TimedActionEffects, type ViewEffect, type ConsumerEffect, type OutgoingMessage, type TimedActionEffect } from "./effects/stateless.ts";
15
- export { View, type ViewClass } from "./view.ts";
15
+ export { View, declaredQuery, tableOf, type DeclaredQuery, type ViewClass } from "./view.ts";
16
+ export { KeyedView, on, type KeyedSource, type KeyedViewClass, type ViewRows } from "./keyedView.ts";
17
+ export { KeyedViewEffects, type KeyedViewEffect, type RowChange } from "./effects/keyed.ts";
16
18
  export { Consumer, ProtocolVersionError, type ConsumerClass } from "./consumer.ts";
19
+ export { Contract, CONTRACT_NAME_RULE, fingerprintOf, type Publication } from "./contract.ts";
17
20
  export { GRAPH_DELTA_SCHEMA, Graph, GraphConsumer, GraphEffects, GraphError, edgeKey, elementKey, graphDeltaCodec, nodeKey, readDelta, type Delta, type Element, type GraphConsumerClass, type GraphEffect, type GraphFault, type Properties, type PropertyValue, type Scalar, type Version, } from "./graph.ts";
18
21
  export { TimedAction, type TimedActionClass } from "./timedAction.ts";
19
22
  export { AgentEffects, type AgentEffect } from "./effects/agent.ts";
20
23
  export { Agent, type AgentClass } from "./agent.ts";
24
+ export { ApprovalAwaited, APPROVALS_SINCE, type AgentOutcome, type Approval, type ApprovalRequest, type DecisionInput } from "./approvals.ts";
25
+ export { mcpServer, resultGuardrail, toolName, type McpServerOptions, type McpServerRef, type ResultGuardrailRef } from "./mcp.ts";
21
26
  export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, rejected, taskAcceptance, taskRule, taskType, type Accepted, type AgentState, type Attachment, type AutonomousAgentClass, type AutonomousSettings, type Notification, type Rejected, type TaskAcceptance, type TaskRule, type TaskSnapshot, type TaskType, } from "./autonomous.ts";
22
27
  export { Endpoint, type EndpointClass } from "./endpoint.ts";
23
- export { Acl, Callers, type CallerMatcher, type CallersAcl, HttpProblem, get, post, put, patch, del, sse, type RouteRef, type RouteOptions, type RouteTable, type Params, type ParamNames, type HttpMethod } from "./routes.ts";
28
+ export { type Socket, SocketClosed } from "./socket.ts";
29
+ export { Acl, Callers, type CallerMatcher, type CallersAcl, HttpProblem, get, post, put, patch, del, sse, sseEvent, type SseEvent, socket, type RouteRef, type RouteOptions, type RouteTable, type Params, type ParamNames, type HttpMethod } from "./routes.ts";
24
30
  export { type CommandContext, type RequestContext, type Principal, type Caller, Query, Headers } from "./context.ts";
25
- export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers, type TimerTarget, type ComponentRef } from "./client.ts";
26
- export { Ankka, ServiceBuilder, Registry, RegistrationError, type ServiceOptions, type RegisteredComponent, type RegisteredEndpoint, type Source, type RegisteredEventSourced, type RegisteredKeyValue, type RegisteredWorkflow, type RegisteredView, type RegisteredConsumer, type RegisteredTimedAction, type RegisteredAgent, } from "./service.ts";
31
+ export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers, Secrets, InMemorySecrets, noSecrets, type TimerTarget, type TimedActionTarget, type ComponentRef } from "./client.ts";
32
+ export { Services, ServiceClient, ScriptedServices, noServices, ServiceError, ServiceUnresolvable, ServiceIdentityMismatch, ServiceUnanswered, ServiceCallFailed, type ServiceResponse, type ServiceRequest, type ServiceHeaders, } from "./services.ts";
33
+ export { Ankka, ServiceBuilder, Registry, RegistrationError, type ServiceOptions, type RegisteredComponent, type RegisteredEndpoint, type Source, type RegisteredEventSourced, type RegisteredKeyValue, type RegisteredWorkflow, type RegisteredView, type RegisteredKeyedView, type RegisteredKeyedSource, type RegisteredConsumer, type RegisteredTimedAction, type RegisteredAgent, } from "./service.ts";
27
34
  export { Server, type ServerOptions } from "./server/server.ts";
28
35
  export { problems as sidecarProblems } from "./server/discovery.ts";
29
36
  export { PROTOCOL_VERSION, SDK_NAME } from "./spec.ts";
37
+ export { StartFrom } from "./startFrom.ts";
30
38
  export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, type Materialised, type MaterialisedKeyValue, type MaterialisedWorkflowCommand, type MaterialisedStep, } from "./materialise.ts";
31
39
  export type { ComponentKind } from "./kinds.ts";
package/dist/index.js CHANGED
@@ -23,19 +23,27 @@ export { KeyValueEntity } from "./keyValueEntity.js";
23
23
  export { WorkflowEffects, StepEffects, WorkflowUpdateBuilder, StepUpdateBuilder, workflowSettings, } from "./effects/workflow.js";
24
24
  export { Workflow } from "./workflow.js";
25
25
  export { ViewEffects, ConsumerEffects, TimedActionEffects } from "./effects/stateless.js";
26
- export { View } from "./view.js";
26
+ export { View, declaredQuery, tableOf } from "./view.js";
27
+ export { KeyedView, on } from "./keyedView.js";
28
+ export { KeyedViewEffects } from "./effects/keyed.js";
27
29
  export { Consumer, ProtocolVersionError } from "./consumer.js";
30
+ export { Contract, CONTRACT_NAME_RULE, fingerprintOf } from "./contract.js";
28
31
  export { GRAPH_DELTA_SCHEMA, Graph, GraphConsumer, GraphEffects, GraphError, edgeKey, elementKey, graphDeltaCodec, nodeKey, readDelta, } from "./graph.js";
29
32
  export { TimedAction } from "./timedAction.js";
30
33
  export { AgentEffects } from "./effects/agent.js";
31
34
  export { Agent } from "./agent.js";
35
+ export { ApprovalAwaited, APPROVALS_SINCE } from "./approvals.js";
36
+ export { mcpServer, resultGuardrail, toolName } from "./mcp.js";
32
37
  export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, rejected, taskAcceptance, taskRule, taskType, } from "./autonomous.js";
33
38
  export { Endpoint } from "./endpoint.js";
34
- export { Acl, Callers, HttpProblem, get, post, put, patch, del, sse } from "./routes.js";
39
+ export { SocketClosed } from "./socket.js";
40
+ export { Acl, Callers, HttpProblem, get, post, put, patch, del, sse, sseEvent, socket } from "./routes.js";
35
41
  export { Query, Headers } from "./context.js";
36
- export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers } from "./client.js";
42
+ export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers, Secrets, InMemorySecrets, noSecrets } from "./client.js";
43
+ export { Services, ServiceClient, ScriptedServices, noServices, ServiceError, ServiceUnresolvable, ServiceIdentityMismatch, ServiceUnanswered, ServiceCallFailed, } from "./services.js";
37
44
  export { Ankka, ServiceBuilder, Registry, RegistrationError, } from "./service.js";
38
45
  export { Server } from "./server/server.js";
39
46
  export { problems as sidecarProblems } from "./server/discovery.js";
40
47
  export { PROTOCOL_VERSION, SDK_NAME } from "./spec.js";
48
+ export { StartFrom } from "./startFrom.js";
41
49
  export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, } from "./materialise.js";
@@ -0,0 +1,56 @@
1
+ import type { Shape } from "./codec.ts";
2
+ import type { Metadata } from "./effects/common.ts";
3
+ import type { ComponentClient, ComponentRef } from "./client.ts";
4
+ import { KeyedViewEffects, type KeyedViewEffect } from "./effects/keyed.ts";
5
+ import type { DeclaredQuery } from "./view.ts";
6
+ type MaybePromise<T> = T | Promise<T>;
7
+ /** A keyed view's own rows, for the length of one change. It reaches no other view. */
8
+ export interface ViewRows<Row> {
9
+ /** The row under `key`, or `null`. */
10
+ get(key: string): Promise<Row | null>;
11
+ /** The rows of one of this view's declared queries, asked with its values. */
12
+ ask(name: string, values?: Readonly<Record<string, string>>): Promise<Row[]>;
13
+ }
14
+ /** One source of a keyed view, and what the view does with each of its changes and with its entity's deletion. */
15
+ export interface KeyedSource<V = any, E = any> {
16
+ readonly source: ComponentRef;
17
+ readonly events: Shape<E>;
18
+ readonly onChange: (view: V, event: E) => MaybePromise<KeyedViewEffect<any>>;
19
+ readonly deleted?: (view: V) => MaybePromise<KeyedViewEffect<any>>;
20
+ }
21
+ /**
22
+ * Declares a source of a keyed view: the entity whose changes it reads, the shape of those changes, and
23
+ * the handler for each. When the entity is deleted the view does nothing unless `deleted` names rows.
24
+ */
25
+ export declare function on<V, E>(source: ComponentRef, events: Shape<E>, onChange: (view: V, event: E) => MaybePromise<KeyedViewEffect<any>>, options?: {
26
+ readonly deleted?: (view: V) => MaybePromise<KeyedViewEffect<any>>;
27
+ }): KeyedSource<V, E>;
28
+ export declare abstract class KeyedView<Row> {
29
+ #private;
30
+ readonly effects: KeyedViewEffects<Row>;
31
+ /** The change's metadata: `ce-subject` is the source entity's id, `ankka.sequence` its sequence number. */
32
+ get metadata(): Metadata;
33
+ /** The id of the entity the change came from. */
34
+ get subject(): string;
35
+ /** This view's own rows, by key or by one of its declared queries. */
36
+ get rows(): ViewRows<Row>;
37
+ get client(): ComponentClient;
38
+ /** @internal */
39
+ get _kind(): "view";
40
+ /** @internal */
41
+ _bind(metadata: Metadata, client: ComponentClient | undefined, rows: ViewRows<Row>): void;
42
+ }
43
+ export interface KeyedViewClass<Row = unknown, C extends KeyedView<Row> = KeyedView<Row>> {
44
+ new (): C;
45
+ readonly componentId: string;
46
+ readonly row: Shape<Row>;
47
+ /** One per entity the view reads, declared with `on(...)`. */
48
+ readonly sources: readonly KeyedSource<C, any>[];
49
+ /** Raised to have the view emptied and every source read again from its beginning. Absent is 1. */
50
+ readonly version?: number;
51
+ /** The queries the view can be asked by name, by its own handlers among others. */
52
+ readonly declared?: readonly DeclaredQuery[];
53
+ }
54
+ /** The rows a keyed view reads through the component client, on its own id. */
55
+ export declare function clientRows<Row>(client: ComponentClient, viewId: string, row: Shape<Row>): ViewRows<Row>;
56
+ export {};
@@ -0,0 +1,67 @@
1
+ // A keyed view: several entities' changes, each through a handler of its own, written to whichever rows
2
+ // a change is about, by key. The sidecar runs one projection per source and handles one change at a
3
+ // time across all of them; this process only says which rows each change writes.
4
+ //
5
+ // export class Shipments extends KeyedView<ShipmentRow> {
6
+ // static readonly componentId = "shipments"
7
+ // static readonly row = ShipmentRowShape
8
+ // static readonly sources = [
9
+ // on(ShipmentEntity, ShipmentEvents, (v: Shipments, e) => v.onShipment(e)),
10
+ // on(CustomerEntity, CustomerEvents, (v: Shipments, e) => v.onCustomer(e)),
11
+ // ]
12
+ // static readonly declared = [declaredQuery("of-customer", `SELECT payload FROM ${tableOf("shipments")} WHERE ...`)]
13
+ // async onCustomer(event: CustomerEvent) {
14
+ // const theirs = await this.rows.ask("of-customer", { customer: this.subject })
15
+ // return this.effects.updateRows(theirs.map((r) => [r.shipmentId, { ...r, name: event.name }]))
16
+ // }
17
+ // }
18
+ import { KeyedViewEffects } from "./effects/keyed.js";
19
+ /**
20
+ * Declares a source of a keyed view: the entity whose changes it reads, the shape of those changes, and
21
+ * the handler for each. When the entity is deleted the view does nothing unless `deleted` names rows.
22
+ */
23
+ export function on(source, events, onChange, options = {}) {
24
+ return Object.freeze({ source, events, onChange, ...(options.deleted ? { deleted: options.deleted } : {}) });
25
+ }
26
+ export class KeyedView {
27
+ effects = new KeyedViewEffects();
28
+ #metadata = {};
29
+ #client;
30
+ #rows;
31
+ /** The change's metadata: `ce-subject` is the source entity's id, `ankka.sequence` its sequence number. */
32
+ get metadata() {
33
+ return this.#metadata;
34
+ }
35
+ /** The id of the entity the change came from. */
36
+ get subject() {
37
+ return this.#metadata["ce-subject"] ?? "";
38
+ }
39
+ /** This view's own rows, by key or by one of its declared queries. */
40
+ get rows() {
41
+ if (!this.#rows)
42
+ throw new Error("rows are only available inside a keyed view's handler");
43
+ return this.#rows;
44
+ }
45
+ get client() {
46
+ if (!this.#client)
47
+ throw new Error("client is only available inside a keyed view's handler");
48
+ return this.#client;
49
+ }
50
+ /** @internal */
51
+ get _kind() {
52
+ return "view";
53
+ }
54
+ /** @internal */
55
+ _bind(metadata, client, rows) {
56
+ this.#metadata = metadata;
57
+ this.#client = client;
58
+ this.#rows = rows;
59
+ }
60
+ }
61
+ /** The rows a keyed view reads through the component client, on its own id. */
62
+ export function clientRows(client, viewId, row) {
63
+ return {
64
+ get: (key) => client.views.get(viewId, key, row),
65
+ ask: (name, values = {}) => client.views.ask(viewId, name, values, row),
66
+ };
67
+ }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { type Approval } from "./approvals.ts";
2
+ export declare const VARIABLE_PREFIX = "ANKKA_MCP_";
3
+ export declare const TOOL_PREFIX = "mcp__";
4
+ type MaybePromise<T> = T | Promise<T>;
5
+ export interface McpServerOptions {
6
+ /** Where the server is; `ANKKA_MCP_<NAME>_URL` replaces it, and is required when neither this nor `service` is given. */
7
+ readonly url?: string;
8
+ /** An ankka service, called as one, at `path` (by default `/mcp`), in `project` (by default this one). */
9
+ readonly service?: string;
10
+ readonly project?: string;
11
+ readonly path?: string;
12
+ /** A header's name and the `ANKKA_MCP_` variable its value is read from. */
13
+ readonly headers?: Readonly<Record<string, string>>;
14
+ /** Every tool of the server waits for a person. */
15
+ readonly approval?: Approval;
16
+ }
17
+ export interface McpServerRef extends McpServerOptions {
18
+ readonly name: string;
19
+ }
20
+ /** An MCP server the agent lists under `name`, lower-case letters, digits and hyphens. */
21
+ export declare function mcpServer(name: string, options?: McpServerOptions): McpServerRef;
22
+ /** Checks what an MCP server's tool answered before the model is told it: a reason to withhold it, or `null`. */
23
+ export interface ResultGuardrailRef {
24
+ readonly name: string;
25
+ readonly check: (tool: string, text: string) => MaybePromise<string | null>;
26
+ }
27
+ export declare function resultGuardrail(name: string, check: (tool: string, text: string) => MaybePromise<string | null>): ResultGuardrailRef;
28
+ /** The name a server's tool is offered to the model under. */
29
+ export declare function toolName(server: string, tool: string): string;
30
+ /** What is wrong with one server, in the sidecar's words. */
31
+ export declare function serverProblems(server: McpServerRef): string[];
32
+ /** The discovery form of a server. */
33
+ export declare function serverToProto(server: McpServerRef): {
34
+ name: string;
35
+ address: {
36
+ case: "url";
37
+ value: string;
38
+ } | {
39
+ case: "service";
40
+ value: {
41
+ name: string;
42
+ path: string;
43
+ project?: string | undefined;
44
+ };
45
+ } | {
46
+ value?: undefined;
47
+ case: undefined;
48
+ };
49
+ headers: {
50
+ name: string;
51
+ variable: string;
52
+ }[];
53
+ approval?: {
54
+ withinMillis?: bigint;
55
+ } | undefined;
56
+ };
57
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,64 @@
1
+ // MCP servers an agent offers the model, and the guardrails that check what they answer.
2
+ //
3
+ // The sidecar connects to each server when it starts, reads its tools and offers them to the model as
4
+ // `mcp__<server>__<tool>`. This process is never asked to run one, and never sees a credential: a
5
+ // header's value is read from a variable on the sidecar, and only `ANKKA_MCP_` variables reach it. What a
6
+ // server's tool answers is put through the agent's result guardrails, which this process answers,
7
+ // before the model is told it.
8
+ //
9
+ // static readonly mcpServers = { tickets: mcpServer("tickets", { headers: { Authorization: "ANKKA_MCP_TICKETS_TOKEN" } }) }
10
+ // static readonly resultGuardrails = { noInstructions: resultGuardrail("no-instructions", (tool, text) => null) }
11
+ import { approvalProblem, approvalToProto } from "./approvals.js";
12
+ export const VARIABLE_PREFIX = "ANKKA_MCP_";
13
+ export const TOOL_PREFIX = "mcp__";
14
+ const NAME = /^[a-z0-9-]+$/;
15
+ /** An MCP server the agent lists under `name`, lower-case letters, digits and hyphens. */
16
+ export function mcpServer(name, options = {}) {
17
+ return Object.freeze({ ...options, name });
18
+ }
19
+ export function resultGuardrail(name, check) {
20
+ if (typeof name !== "string" || name.trim() === "")
21
+ throw new TypeError("a result guardrail needs a name");
22
+ return Object.freeze({ name, check });
23
+ }
24
+ /** The name a server's tool is offered to the model under. */
25
+ export function toolName(server, tool) {
26
+ return `${TOOL_PREFIX}${server}__${tool}`;
27
+ }
28
+ /** What is wrong with one server, in the sidecar's words. */
29
+ export function serverProblems(server) {
30
+ const found = [];
31
+ const name = server.name;
32
+ if (!NAME.test(name))
33
+ found.push(`an MCP server's name is lower-case letters, digits and hyphens ([a-z0-9-]), not '${name}'`);
34
+ if (server.url !== undefined && server.service !== undefined)
35
+ found.push(`MCP server '${name}' has both a URL and a service; give one`);
36
+ if (server.url !== undefined && server.url.trim() === "")
37
+ found.push(`MCP server '${name}' needs a URL`);
38
+ for (const [header, variable] of Object.entries(server.headers ?? {})) {
39
+ if (header.trim() === "")
40
+ found.push(`MCP server '${name}' has a header with no name`);
41
+ else if (!variable.startsWith(VARIABLE_PREFIX)) {
42
+ found.push(`MCP server '${name}': the header '${header}' takes its value from '${variable}', which must start ${VARIABLE_PREFIX} — only those variables reach the platform's program, which is what connects to the server`);
43
+ }
44
+ }
45
+ const approval = approvalProblem(server.approval);
46
+ if (approval)
47
+ found.push(`MCP server '${name}': ${approval}`);
48
+ return found;
49
+ }
50
+ /** The discovery form of a server. */
51
+ export function serverToProto(server) {
52
+ const approval = approvalToProto(server.approval);
53
+ const address = server.url !== undefined
54
+ ? { case: "url", value: server.url }
55
+ : server.service !== undefined
56
+ ? { case: "service", value: { name: server.service, path: server.path ?? "/mcp", ...(server.project !== undefined ? { project: server.project } : {}) } }
57
+ : { case: undefined };
58
+ return {
59
+ name: server.name,
60
+ address,
61
+ headers: Object.entries(server.headers ?? {}).map(([name, variable]) => ({ name, variable })),
62
+ ...(approval !== undefined ? { approval } : {}),
63
+ };
64
+ }
package/dist/routes.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Socket } from "./socket.ts";
1
2
  import type { Shape } from "./codec.ts";
2
3
  import type { RequestContext } from "./context.ts";
3
4
  import type { Infer, Schema } from "./schema.ts";
@@ -74,8 +75,10 @@ export interface RouteRef<Ep = unknown, P = unknown, B = unknown, R = unknown> {
74
75
  readonly body: Shape<B> | undefined;
75
76
  readonly reply: Shape<R> | undefined;
76
77
  readonly streaming: boolean;
78
+ /** Answered by opening a socket (protocol 1.9); `run`'s third argument is then the `Socket`. */
79
+ readonly socket: boolean;
77
80
  readonly acl: Acl | undefined;
78
- readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string>;
81
+ readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string | SseEvent>;
79
82
  }
80
83
  export declare function parseTemplate(template: string): string[];
81
84
  type NoBodyRun<Ep, T extends string, PS, R> = (self: Ep, request: RequestContext<Params<T, PS>>) => MaybePromise<R>;
@@ -100,8 +103,25 @@ export declare const del: {
100
103
  <Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
101
104
  <Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
102
105
  };
103
- /** A server-sent-events route: the handler returns an `AsyncIterable<string>`, one frame per string. */
104
- export declare function sse<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>) => AsyncIterable<string> | Promise<AsyncIterable<string>>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, never>;
106
+ /**
107
+ * A server-sent event under a name of its own, which an `sse` handler may yield beside text: an agent
108
+ * turn that stops for approval ends its stream with `sseEvent("approval", requests)`. `value` is sent as
109
+ * JSON. Needs protocol 1.11 of the runtime.
110
+ */
111
+ export interface SseEvent {
112
+ readonly kind: "sse-event";
113
+ readonly name: string;
114
+ readonly value: unknown;
115
+ }
116
+ export declare function sseEvent(name: string, value: unknown): SseEvent;
117
+ /** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
118
+ export declare function sse<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>) => AsyncIterable<string | SseEvent> | Promise<AsyncIterable<string | SseEvent>>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, never>;
119
+ /**
120
+ * A socket route: `socket("/{room}", async (ep: Rooms, req, socket) => { for await (const t of socket) await socket.send(t) })`.
121
+ * The handler runs for as long as the socket is open, with the opening request as `req` throughout.
122
+ * Needs protocol 1.9 of the runtime.
123
+ */
124
+ export declare function socket<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>, socket: Socket) => Promise<void>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, Socket, void>;
105
125
  /** The routes table type an endpoint declares. */
106
126
  export type RouteTable<Ep> = Readonly<Record<string, RouteRef<Ep, any, any, any>>>;
107
127
  export {};
package/dist/routes.js CHANGED
@@ -57,7 +57,7 @@ export function parseTemplate(template) {
57
57
  throw new TypeError(`route template ${template} repeats a parameter`);
58
58
  return names;
59
59
  }
60
- function route(method, template, body, reply, run, options, streaming) {
60
+ function route(method, template, body, reply, run, options, streaming, socket = false) {
61
61
  const paramNames = parseTemplate(template);
62
62
  if (typeof run !== "function")
63
63
  throw new TypeError(`route ${method} ${template}: the handler is not a function`);
@@ -68,7 +68,7 @@ function route(method, template, body, reply, run, options, streaming) {
68
68
  if (!paramNames.includes(name))
69
69
  throw new TypeError(`route ${method} ${template}: params names ${name}, which is not in the template`);
70
70
  }
71
- return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, acl: options?.acl, run });
71
+ return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, socket, acl: options?.acl, run });
72
72
  }
73
73
  function isShape(x) {
74
74
  return typeof x === "object" && x !== null && (typeof x.kind === "string" || isCodec(x));
@@ -96,7 +96,20 @@ export const put = withBody("PUT");
96
96
  export const patch = withBody("PATCH");
97
97
  /** A DELETE route; `delete` is a reserved word, so `del`. */
98
98
  export const del = withBody("DELETE");
99
- /** A server-sent-events route: the handler returns an `AsyncIterable<string>`, one frame per string. */
99
+ export function sseEvent(name, value) {
100
+ if (typeof name !== "string" || name === "" || /[\r\n]/.test(name))
101
+ throw new TypeError(`an event name must be one non-empty line, not ${JSON.stringify(name)}`);
102
+ return Object.freeze({ kind: "sse-event", name, value });
103
+ }
104
+ /** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
100
105
  export function sse(template, run, options) {
101
106
  return route("GET", template, undefined, undefined, run, options, true);
102
107
  }
108
+ /**
109
+ * A socket route: `socket("/{room}", async (ep: Rooms, req, socket) => { for await (const t of socket) await socket.send(t) })`.
110
+ * The handler runs for as long as the socket is open, with the opening request as `req` throughout.
111
+ * Needs protocol 1.9 of the runtime.
112
+ */
113
+ export function socket(template, run, options) {
114
+ return route("GET", template, undefined, undefined, run, options, false, true);
115
+ }
@@ -84,7 +84,10 @@ export async function handleTool(req, ctx) {
84
84
  if (!registered)
85
85
  return create(ToolResultSchema, { result: { case: "error", value: `no agent ${JSON.stringify(req.componentId)} is registered` } });
86
86
  const agent = new registered.cls();
87
- agent._bind(req.sessionId, {}, ctx.client);
87
+ // What the sidecar says about the tool's run: its trace, and the agent's handler as the caller of
88
+ // whatever the tool calls. A sidecar before 1.3 sends none.
89
+ const metadata = metadataFromProto(req.metadata);
90
+ agent._bind(req.sessionId, metadata, ctx.client.withMetadata(metadata));
88
91
  try {
89
92
  return create(ToolResultSchema, { result: { case: "ok", value: await runTool(registered, agent, req.tool, req.argumentsJson) } });
90
93
  }
@@ -95,11 +98,16 @@ export async function handleTool(req, ctx) {
95
98
  }
96
99
  export async function handleGuardrail(req, ctx) {
97
100
  const registered = ctx.registry.of("agent", req.componentId) ?? ctx.registry.of("autonomous-agent", req.componentId);
98
- const guardrail = registered?.guardrails.get(req.guardrail);
101
+ // RESULT (1.11): what an MCP server's tool answered, checked by one of the agent's result guardrails,
102
+ // which are declared apart from its other guardrails.
103
+ const result = req.stage === GuardrailRequest_Stage.RESULT;
104
+ const guardrail = result ? registered?.resultGuardrails.get(req.guardrail) : registered?.guardrails.get(req.guardrail);
99
105
  if (!registered || !guardrail)
100
106
  return create(GuardrailResultSchema, { result: { case: "block", value: `no guardrail ${JSON.stringify(req.guardrail)} on ${req.componentId}` } });
101
107
  try {
102
- const reason = await guardrail.check(req.stage === GuardrailRequest_Stage.OUTPUT ? "output" : "input", req.text);
108
+ const reason = result
109
+ ? await guardrail.check(req.tool ?? "", req.text)
110
+ : await guardrail.check(req.stage === GuardrailRequest_Stage.OUTPUT ? "output" : "input", req.text);
103
111
  return reason === null || reason === undefined
104
112
  ? create(GuardrailResultSchema, { result: { case: "pass", value: {} } })
105
113
  : create(GuardrailResultSchema, { result: { case: "block", value: reason } });
@@ -114,7 +122,7 @@ async function handleAutonomousTool(registered, req, ctx) {
114
122
  if (!tool)
115
123
  return create(ToolResultSchema, { result: { case: "error", value: `no tool ${JSON.stringify(req.tool)} on ${registered.id}` } });
116
124
  const agent = new registered.cls();
117
- agent._bind(req.sessionId, ctx.client);
125
+ agent._bind(req.sessionId, ctx.client.withMetadata(metadataFromProto(req.metadata)));
118
126
  try {
119
127
  let input;
120
128
  if (isCodec(tool.input))
@@ -1,5 +1,16 @@
1
- import type { ConnectRouter } from "@connectrpc/connect";
1
+ import { type ConnectRouter } from "@connectrpc/connect";
2
2
  import { type Spec } from "../_proto/ankka/protocol/v1/discovery_pb.ts";
3
+ /**
4
+ * A sidecar older than 1.7 would ignore where a topic source starts and its version: a consumer
5
+ * declared `latest` would read everything, and a raised version would rebuild nothing. Refused,
6
+ * naming what declares them, rather than served wrong.
7
+ */
8
+ export declare function refusal(spec: Spec, sidecarProtocol: string): string | undefined;
3
9
  /** Every problem the sidecar has reported to this process, in order. */
4
10
  export declare const problems: string[];
5
11
  export declare function discoveryRoutes(router: ConnectRouter, spec: () => Spec, log?: (message: string) => void): void;
12
+ /** The protocol version whose runtimes serve socket routes. */
13
+ export declare const SOCKETS_SINCE: readonly [number, number];
14
+ /** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.9
15
+ * does not know a socket route, and would serve it as a plain GET. */
16
+ export declare function socketRefusal(spec: Spec, runtime: string): string | undefined;