ankka 0.7.1 → 0.9.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/service.js CHANGED
@@ -11,8 +11,9 @@ import { View } from "./view.js";
11
11
  import { Consumer } from "./consumer.js";
12
12
  import { TimedAction } from "./timedAction.js";
13
13
  import { Agent } from "./agent.js";
14
+ import { AutonomousAgent, COMPLETE_TASK, FAIL_TASK } from "./autonomous.js";
14
15
  import { Endpoint } from "./endpoint.js";
15
- import { Acl as AclValues } from "./routes.js";
16
+ import { isAcl } from "./routes.js";
16
17
  import { ComponentClient } from "./client.js";
17
18
  import { Server } from "./server/server.js";
18
19
  import { renderSpec } from "./spec.js";
@@ -100,6 +101,8 @@ export class ServiceBuilder {
100
101
  add(registerTimedAction(cls, problems), cls);
101
102
  else if (extendsBase(cls, Agent))
102
103
  add(registerAgent(cls, problems), cls);
104
+ else if (extendsBase(cls, AutonomousAgent))
105
+ add(registerAutonomousAgent(cls, problems), cls);
103
106
  else if (extendsBase(cls, Endpoint)) {
104
107
  const ep = cls;
105
108
  if (typeof ep.prefix === "string") {
@@ -117,7 +120,7 @@ export class ServiceBuilder {
117
120
  }
118
121
  }
119
122
  else {
120
- problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, Consumer, TimedAction, Agent or Endpoint`);
123
+ problems.push(`${nameOf(cls)} is not a component class: it must extend EventSourcedEntity, KeyValueEntity, Workflow, View, Consumer, TimedAction, Agent, AutonomousAgent or Endpoint`);
121
124
  }
122
125
  }
123
126
  if (problems.length > 0)
@@ -314,6 +317,44 @@ function registerAgent(cls, problems) {
314
317
  return undefined;
315
318
  return Object.freeze({ kind: "agent", id: cls.componentId, cls, handlers, tools, guardrails, role: cls.role ?? "", maxToolCallSteps });
316
319
  }
320
+ function registerAutonomousAgent(cls, problems) {
321
+ const { fail, ok } = checker(cls, problems);
322
+ requireId(cls, fail);
323
+ if (typeof cls.description !== "string" || cls.description.trim() === "")
324
+ fail("a description is required");
325
+ const tools = new Map();
326
+ for (const [property, t] of Object.entries(cls.tools ?? {})) {
327
+ if (typeof t !== "object" || t === null || typeof t.name !== "string" || typeof t.run !== "function") {
328
+ fail(`tools.${property} is not a tool: declare it with tool(name, description, input, run)`);
329
+ continue;
330
+ }
331
+ if (t.name === COMPLETE_TASK || t.name === FAIL_TASK)
332
+ fail(`tool name ${JSON.stringify(t.name)} is reserved`);
333
+ if (tools.has(t.name))
334
+ fail(`two tools declare the name ${JSON.stringify(t.name)}`);
335
+ tools.set(t.name, t);
336
+ }
337
+ const guardrails = new Map();
338
+ for (const g of Object.values(cls.guardrails ?? {})) {
339
+ if (guardrails.has(g.name))
340
+ fail(`two guardrails declare the name ${JSON.stringify(g.name)}`);
341
+ guardrails.set(g.name, g);
342
+ }
343
+ const taskTypes = new Map();
344
+ const accepts = cls.accepts ?? [];
345
+ if (accepts.length === 0)
346
+ fail("it accepts no task type: declare accepts = [taskAcceptance(...)]");
347
+ for (const a of accepts) {
348
+ if (taskTypes.has(a.taskType.name))
349
+ fail(`task type ${JSON.stringify(a.taskType.name)} is accepted twice`);
350
+ if (!Number.isInteger(a.maxIterations) || a.maxIterations < 1)
351
+ fail(`task type ${JSON.stringify(a.taskType.name)} needs a budget of at least one iteration`);
352
+ taskTypes.set(a.taskType.name, a.taskType);
353
+ }
354
+ if (!ok())
355
+ return undefined;
356
+ return Object.freeze({ kind: "autonomous-agent", id: cls.componentId, cls, tools, guardrails, taskTypes });
357
+ }
317
358
  /** Collects a handler table by wire name, reporting a duplicate wire name or a kind the component cannot host. */
318
359
  export function collectHandlers(table, what, allowed, fail) {
319
360
  const out = new Map();
@@ -347,8 +388,8 @@ function registerEndpoint(cls, problems) {
347
388
  if (typeof cls.prefix !== "string" || !cls.prefix.startsWith("/") || (cls.prefix.length > 1 && cls.prefix.endsWith("/"))) {
348
389
  fail(`needs a static prefix starting with "/" and not ending with one, not ${JSON.stringify(cls.prefix)}`);
349
390
  }
350
- if (!Object.values(AclValues).includes(cls.acl)) {
351
- fail(`needs a static acl — Acl.allowAll, Acl.denyAll or Acl.authenticated. An unstated access rule is a decision nobody made`);
391
+ if (!isAcl(cls.acl)) {
392
+ fail(`needs a static acl — Acl.allowAll, Acl.denyAll, Acl.authenticated or Acl.allowCallers(...). An unstated access rule is a decision nobody made`);
352
393
  }
353
394
  const routes = new Map();
354
395
  const seen = new Map();
@@ -367,7 +408,7 @@ function registerEndpoint(cls, problems) {
367
408
  if (other)
368
409
  fail(`routes ${other} and ${id} both declare ${key}`);
369
410
  seen.set(key, id);
370
- if (r.acl !== undefined && !Object.values(AclValues).includes(r.acl))
411
+ if (r.acl !== undefined && !isAcl(r.acl))
371
412
  fail(`routes.${id}: unknown acl ${JSON.stringify(r.acl)}`);
372
413
  routes.set(id, r);
373
414
  }
package/dist/spec.d.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { Endpoint_Acl, type Spec } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
1
+ import { type MessageInitShape } from "@bufbuild/protobuf";
2
+ import { Endpoint_Acl, type CallerMatcherSchema, type Spec } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
2
3
  import type { Registry } from "./service.ts";
3
- import { Acl } from "./routes.ts";
4
- export declare const PROTOCOL_VERSION = "1.0";
4
+ import type { Acl } from "./routes.ts";
5
+ export declare const PROTOCOL_VERSION = "1.2";
5
6
  export declare const SDK_NAME = "ankka-typescript";
6
7
  export declare function aclToProto(acl: Acl): Endpoint_Acl;
8
+ /** The callers a CALLERS acl names, as discovery carries them; empty for every other rule. */
9
+ export declare function callersToProto(acl: Acl): MessageInitShape<typeof CallerMatcherSchema>[];
7
10
  export declare function renderSpec(registry: Registry): Spec;
package/dist/spec.js CHANGED
@@ -1,23 +1,42 @@
1
1
  // Renders the discovery Spec from the registry: what the sidecar hosts is exactly what is registered.
2
2
  import { create } from "@bufbuild/protobuf";
3
3
  import { Endpoint_Acl, SpecSchema } from "./_proto/ankka/protocol/v1/discovery_pb.js";
4
+ import { resultSchemaJson } from "./autonomous.js";
4
5
  import { kindToProto } from "./kinds.js";
5
- import { Acl } from "./routes.js";
6
6
  import { toJsonSchema } from "./schema.js";
7
7
  import { isCodec } from "./codec.js";
8
8
  import { VERSION } from "./version.js";
9
- export const PROTOCOL_VERSION = "1.0";
9
+ export const PROTOCOL_VERSION = "1.2";
10
10
  export const SDK_NAME = "ankka-typescript";
11
11
  export function aclToProto(acl) {
12
+ if (typeof acl === "object")
13
+ return Endpoint_Acl.CALLERS;
12
14
  switch (acl) {
13
- case Acl.allowAll:
15
+ case "allow-all":
14
16
  return Endpoint_Acl.ALLOW_ALL;
15
- case Acl.denyAll:
17
+ case "deny-all":
16
18
  return Endpoint_Acl.DENY_ALL;
17
- case Acl.authenticated:
19
+ case "authenticated":
18
20
  return Endpoint_Acl.AUTHENTICATED;
19
21
  }
20
22
  }
23
+ /** The callers a CALLERS acl names, as discovery carries them; empty for every other rule. */
24
+ export function callersToProto(acl) {
25
+ if (typeof acl !== "object")
26
+ return [];
27
+ return acl.callers.map((m) => {
28
+ switch (m.kind) {
29
+ case "internet":
30
+ return { kind: { case: "internet", value: {} } };
31
+ case "anyInProject":
32
+ return { kind: { case: "anyInProject", value: {} } };
33
+ case "self":
34
+ return { kind: { case: "self", value: {} } };
35
+ case "service":
36
+ return { kind: { case: "service", value: m.project !== undefined ? { name: m.name, project: m.project } : { name: m.name } } };
37
+ }
38
+ });
39
+ }
21
40
  function handlerInits(handlers) {
22
41
  return [...handlers.values()]
23
42
  .map((h) => ({ name: h.name, readOnly: h.readOnly, streaming: h.streaming }))
@@ -48,6 +67,8 @@ function settingsInit(s) {
48
67
  function componentInit(c) {
49
68
  const base = { kind: kindToProto(c.kind), id: c.id };
50
69
  switch (c.kind) {
70
+ case "autonomous-agent":
71
+ return autonomousInit(c);
51
72
  case "event-sourced":
52
73
  return { ...base, handlers: handlerInits(c.handlers), detail: { case: "eventSourced", value: { snapshotEvery: c.snapshotEvery } } };
53
74
  case "key-value":
@@ -82,19 +103,54 @@ function componentInit(c) {
82
103
  };
83
104
  }
84
105
  }
106
+ function settingsOf(s) {
107
+ return {
108
+ ...(s.approachingBudgetAt !== undefined ? { approachingBudgetAt: s.approachingBudgetAt } : {}),
109
+ ...(s.repeatedFailureAt !== undefined ? { repeatedFailureAt: s.repeatedFailureAt } : {}),
110
+ ...(s.maxConsecutiveFailures !== undefined ? { maxConsecutiveFailures: s.maxConsecutiveFailures } : {}),
111
+ ...(s.dependencyStuckAfterMillis !== undefined ? { dependencyStuckAfterMillis: BigInt(s.dependencyStuckAfterMillis) } : {}),
112
+ };
113
+ }
114
+ function autonomousInit(c) {
115
+ const cls = c.cls;
116
+ return {
117
+ kind: kindToProto(c.kind),
118
+ id: c.id,
119
+ handlers: [],
120
+ detail: {
121
+ case: "autonomousAgent",
122
+ value: {
123
+ description: cls.description,
124
+ ...(cls.instructions !== undefined ? { instructions: cls.instructions } : {}),
125
+ ...(cls.model !== undefined ? { model: cls.model } : {}),
126
+ tools: [...c.tools.values()]
127
+ .sort((a, b) => a.name.localeCompare(b.name))
128
+ .map((t) => ({ name: t.name, description: t.description, inputSchemaJson: JSON.stringify(isCodec(t.input) ? { type: "object" } : toJsonSchema(t.input)) })),
129
+ guardrails: [...c.guardrails.keys()].sort(),
130
+ taskTypes: [...c.taskTypes.values()].map((t) => {
131
+ const schema = resultSchemaJson(t);
132
+ return { name: t.name, description: t.description, rules: t.rules.map((r) => r.name), ...(schema !== undefined ? { resultSchemaJson: schema } : {}) };
133
+ }),
134
+ accepts: cls.accepts.map((a) => ({ taskType: a.taskType.name, maxIterations: a.maxIterations })),
135
+ ...(cls.settings ? { settings: settingsOf(cls.settings) } : {}),
136
+ },
137
+ },
138
+ };
139
+ }
85
140
  export function renderSpec(registry) {
86
141
  const components = [...registry.components.values()].sort((a, b) => a.id.localeCompare(b.id)).map(componentInit);
87
142
  const endpoints = [...registry.endpoints.values()].map((e) => ({
88
143
  id: e.id,
89
144
  prefix: e.prefix,
90
145
  acl: aclToProto(e.acl),
146
+ allowCallers: callersToProto(e.acl),
91
147
  routes: [...e.routes.entries()].map(([id, r]) => ({
92
148
  id,
93
149
  method: r.method,
94
150
  template: r.template,
95
151
  hasBody: r.body !== undefined,
96
152
  streaming: r.streaming,
97
- ...(r.acl !== undefined ? { acl: aclToProto(r.acl) } : {}),
153
+ ...(r.acl !== undefined ? { acl: aclToProto(r.acl), allowCallers: callersToProto(r.acl) } : {}),
98
154
  })),
99
155
  }));
100
156
  return create(SpecSchema, {
@@ -223,7 +223,11 @@ export class AnkkaTestKit {
223
223
  ...this.#env,
224
224
  })
225
225
  .withExposedPorts(HTTP_PORT, CALLBACK_PORT)
226
- .withWaitStrategy(tc.Wait.forHttp("/_ankka/health", HTTP_PORT).forStatusCode(200))
226
+ // Both ports, not only the health route: it answers as soon as HTTP is bound, which can be
227
+ // before the callback server listens, and Docker's port proxy accepts then resets a
228
+ // connection to a port nothing in the container listens on yet — so a handler's first call
229
+ // after a restart failed UNAVAILABLE. forListeningPorts checks from inside the container.
230
+ .withWaitStrategy(tc.Wait.forAll([tc.Wait.forHttp("/_ankka/health", HTTP_PORT).forStatusCode(200), tc.Wait.forListeningPorts()]))
227
231
  .withStartupTimeout(this.#readyTimeoutMs)
228
232
  .start();
229
233
  this.#sidecar = container;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.7.1";
1
+ export declare const VERSION = "0.9.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Written by scripts/proto.ts from package.json; do not edit. 0.0.0 in the tree, the tag's version in a release.
2
- export const VERSION = "0.7.1";
2
+ export const VERSION = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ankka",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Build services on ankka in TypeScript: entities, views, workflows, agents and endpoints hosted by the ankka sidecar.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",