ankka 0.8.0 → 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.
@@ -0,0 +1,199 @@
1
+ // Autonomous agents: a durable agent that is handed a task and works it on its own, iteration by
2
+ // iteration, until it completes the task with a result its type's rules accept, fails it, or spends
3
+ // its budget. This process declares the agent — what it is for, its tools and guardrails, the task
4
+ // types it accepts — and the sidecar runs the loop, the model and every record. The process is asked
5
+ // to run a tool, check a guardrail or check a task rule, each naming the task it is for.
6
+ //
7
+ // export const ANSWER = taskType("answer", "Answer a question", { result: Answer, rules: [taskRule("cites-sources", (a) => a.sources.length ? accepted() : rejected("cite something"))] })
8
+ // export class Answerer extends AutonomousAgent {
9
+ // static readonly componentId = "answerer"
10
+ // static readonly description = "Answers questions"
11
+ // static readonly tools = { lookup: tool("lookup", "Looks a topic up", Topic, (a: Answerer, t) => a.lookup(t)) }
12
+ // static readonly accepts = [taskAcceptance(ANSWER, { maxIterations: 5 })]
13
+ // }
14
+ //
15
+ // A tool may run more than once for one request of the model — after a crash, the last recorded
16
+ // request's tools run again — so a tool with a side effect should tolerate a repeat.
17
+ import { decodeJsonValue, reviver } from "./json.js";
18
+ import { toJsonSchema } from "./schema.js";
19
+ export const COMPLETE_TASK = "complete_task";
20
+ export const FAIL_TASK = "fail_task";
21
+ export const accepted = () => Object.freeze({ verdict: "accepted" });
22
+ export const rejected = (reason) => Object.freeze({ verdict: "rejected", reason });
23
+ export function taskRule(name, check) {
24
+ if (typeof name !== "string" || name.trim() === "")
25
+ throw new TypeError("a task rule needs a name");
26
+ return Object.freeze({ name, check });
27
+ }
28
+ export function taskType(name, description, options = {}) {
29
+ if (typeof name !== "string" || name.trim() === "")
30
+ throw new TypeError("a task type needs a name");
31
+ if (typeof description !== "string" || description.trim() === "")
32
+ throw new TypeError(`task type ${name} needs a description`);
33
+ const rules = options.rules ?? [];
34
+ const names = rules.map((r) => r.name);
35
+ if (new Set(names).size !== names.length)
36
+ throw new TypeError(`task type ${name} declares a rule twice`);
37
+ return Object.freeze({ name, description, result: options.result, rules: Object.freeze([...rules]) });
38
+ }
39
+ /** Decodes a stored result as the task type's. */
40
+ export function decodeResult(type, stored) {
41
+ const json = JSON.parse(stored, reviver);
42
+ return type.result ? decodeJsonValue(type.result, json) : json;
43
+ }
44
+ export function taskAcceptance(type, options = {}) {
45
+ return Object.freeze({ taskType: type, maxIterations: options.maxIterations ?? 10 });
46
+ }
47
+ export class AutonomousAgent {
48
+ #sessionId = "";
49
+ #client;
50
+ /** The task a tool, a guardrail or a rule is running for. */
51
+ get taskId() {
52
+ return this.#sessionId.startsWith("task:") ? this.#sessionId.slice("task:".length) : this.#sessionId;
53
+ }
54
+ get client() {
55
+ if (!this.#client)
56
+ throw new Error("client is only available inside a tool, a guardrail or a rule");
57
+ return this.#client;
58
+ }
59
+ /** @internal */
60
+ get _kind() {
61
+ return "autonomous-agent";
62
+ }
63
+ /** @internal */
64
+ _bind(sessionId, client) {
65
+ this.#sessionId = sessionId;
66
+ this.#client = client;
67
+ }
68
+ }
69
+ /** The discovery form of a result schema. */
70
+ export function resultSchemaJson(type) {
71
+ return type.result ? JSON.stringify(toJsonSchema(type.result)) : undefined;
72
+ }
73
+ const TASK = "ankka-task";
74
+ const INSTANCE = "ankka-agent-instance";
75
+ const ENDED = new Set(["completed", "failed", "cancelled"]);
76
+ /** Creates tasks: `await client.tasks.create(ANSWER, "How many?")` answers the new task's id. */
77
+ export class Tasks {
78
+ #client;
79
+ constructor(client) {
80
+ this.#client = client;
81
+ }
82
+ /** A dependency that does not exist is refused before anything is written; one that has already ended cancels the new task. */
83
+ async create(type, instructions, options = {}) {
84
+ const id = options.id ?? crypto.randomUUID();
85
+ const dependencies = [...new Set(options.dependsOn ?? [])];
86
+ for (const dep of dependencies)
87
+ await this.#client._raw("event-sourced", TASK, dep, "get");
88
+ await this.#client._raw("event-sourced", TASK, id, "create", {
89
+ typeName: type.name,
90
+ instructions,
91
+ attachments: (options.attachments ?? []).map((a) => ({
92
+ name: a.name,
93
+ contentType: a.contentType,
94
+ content: a.uri !== undefined ? { type: "Reference", uri: a.uri } : { type: "Inline", text: a.content ?? "" },
95
+ })),
96
+ dependencies,
97
+ });
98
+ for (const dep of dependencies) {
99
+ const answer = (await this.#client._raw("event-sourced", TASK, dep, "add-dependent", { taskId: id }));
100
+ if (answer?.alreadyEnded) {
101
+ await this.#client._raw("event-sourced", TASK, id, "cancel", { reason: `dependency '${dep}' ${answer.alreadyEnded}` });
102
+ break;
103
+ }
104
+ }
105
+ return id;
106
+ }
107
+ }
108
+ /** Calls about one task. */
109
+ export class TaskCalls {
110
+ #client;
111
+ taskId;
112
+ constructor(client, taskId) {
113
+ this.#client = client;
114
+ this.taskId = taskId;
115
+ }
116
+ async get(type) {
117
+ const record = (await this.#client._raw("event-sourced", TASK, this.taskId, "get"));
118
+ if (type && record.typeName !== type.name)
119
+ throw new TypeError(`task '${this.taskId}' is a '${record.typeName}' task, not '${type.name}'`);
120
+ const stored = record.result;
121
+ return Object.freeze({
122
+ id: record.id,
123
+ typeName: record.typeName,
124
+ status: record.status,
125
+ result: stored === undefined || stored === null ? undefined : type ? decodeResult(type, stored) : JSON.parse(stored),
126
+ reason: record.reason ?? undefined,
127
+ iterations: record.iterations ?? 0,
128
+ assignee: record.assignee ?? undefined,
129
+ record,
130
+ });
131
+ }
132
+ /** Waits until the task has completed, failed or been cancelled. */
133
+ async wait(type, options = {}) {
134
+ const deadline = Date.now() + (options.timeoutMs ?? 600_000);
135
+ for (;;) {
136
+ const snapshot = await this.get(type);
137
+ if (ENDED.has(snapshot.status))
138
+ return snapshot;
139
+ if (Date.now() > deadline)
140
+ throw new Error(`task '${this.taskId}' had not ended in time; it is ${snapshot.status}`);
141
+ await new Promise((resolve) => setTimeout(resolve, 250));
142
+ }
143
+ }
144
+ /** Cancels the task: at once when waiting, at the end of the current iteration when being worked. */
145
+ async cancel(reason = "cancelled by caller") {
146
+ await this.#client._raw("event-sourced", TASK, this.taskId, "cancel", { reason });
147
+ const record = (await this.#client._raw("event-sourced", TASK, this.taskId, "get"));
148
+ if (record.assignee) {
149
+ await this.#client._raw("autonomous-agent", record.assignee.componentId, record.assignee.instanceId, "dequeue", { taskId: this.taskId, reason });
150
+ }
151
+ }
152
+ }
153
+ /** Calls to one instance of an autonomous agent, by the id the caller chose; with no id, only `runSingleTask`. */
154
+ export class AutonomousAgentCalls {
155
+ #client;
156
+ componentId;
157
+ instanceId;
158
+ constructor(client, componentId, instanceId) {
159
+ this.#client = client;
160
+ this.componentId = componentId;
161
+ this.instanceId = instanceId;
162
+ }
163
+ #instance() {
164
+ if (!this.instanceId)
165
+ throw new TypeError("this call needs an instance id: forAutonomousAgent(agent, instanceId)");
166
+ return this.instanceId;
167
+ }
168
+ /** Creates the task, starts an instance on it, and answers the task's id at once. */
169
+ async runSingleTask(type, instructions, options = {}) {
170
+ const taskId = await new Tasks(this.#client).create(type, instructions, options);
171
+ await this.#client._raw("autonomous-agent", this.componentId, crypto.randomUUID(), "run-single-task", { taskId });
172
+ return taskId;
173
+ }
174
+ assign(...taskIds) {
175
+ return this.#client._raw("autonomous-agent", this.componentId, this.#instance(), "assign", { taskIds });
176
+ }
177
+ async suspend() {
178
+ await this.#client._raw("autonomous-agent", this.componentId, this.#instance(), "suspend");
179
+ }
180
+ async resume() {
181
+ await this.#client._raw("autonomous-agent", this.componentId, this.#instance(), "resume");
182
+ }
183
+ /** Stops the instance for good; its tasks go back to pending for another to take. */
184
+ async terminate() {
185
+ await this.#client._raw("autonomous-agent", this.componentId, this.#instance(), "terminate");
186
+ }
187
+ async state() {
188
+ const r = (await this.#client._raw("event-sourced", INSTANCE, `${this.componentId}/${this.#instance()}`, "get"));
189
+ const current = r.current;
190
+ const phase = r.terminated ? "terminated" : r.suspended ? "suspended" : current ? "working" : (r.queue ?? []).length > 0 ? "waiting" : "idle";
191
+ return Object.freeze({ phase, currentTask: current?.taskId, iteration: current?.iteration ?? 0, queued: [...(r.queue ?? [])] });
192
+ }
193
+ /** What the instance does from now on, as it happens. Nothing is replayed. */
194
+ async *notifications() {
195
+ for await (const text of this.#client._rawStream("autonomous-agent", this.componentId, this.#instance(), "notifications")) {
196
+ yield JSON.parse(text);
197
+ }
198
+ }
199
+ }
package/dist/client.d.ts CHANGED
@@ -5,6 +5,7 @@ import { type Metadata } from "./effects/common.ts";
5
5
  import type { HandlerRef } from "./handlers.ts";
6
6
  import { type ComponentKind } from "./kinds.ts";
7
7
  import type { Duration } from "./time.ts";
8
+ import { AutonomousAgentCalls, TaskCalls, Tasks } from "./autonomous.ts";
8
9
  /** A component class as the typed client sees it: an id and, on its prototype, its kind. */
9
10
  export interface ComponentRef {
10
11
  readonly componentId: string;
@@ -82,6 +83,18 @@ export declare class ComponentClient {
82
83
  forKeyValueEntity(componentId: string, entityId: string): Calls;
83
84
  forWorkflow(componentId: string, workflowId: string): Calls;
84
85
  forAgent(componentId: string, sessionId: string): Calls;
86
+ /** Creates tasks for autonomous agents. */
87
+ get tasks(): Tasks;
88
+ /** Calls about one task. */
89
+ forTask(taskId: string): TaskCalls;
90
+ /** One instance of an autonomous agent, by the id the caller chose; without one, only `runSingleTask`, on an instance the platform names. */
91
+ forAutonomousAgent(agent: {
92
+ readonly componentId: string;
93
+ } | string, instanceId?: string): AutonomousAgentCalls;
94
+ /** @internal A call with a JSON body and a JSON reply, as the platform's own components take them. */
95
+ _raw(kind: ComponentKind, componentId: string, entityId: string, name: string, body?: unknown): Promise<unknown>;
96
+ /** @internal A streaming call whose tokens are text. */
97
+ _rawStream(kind: ComponentKind, componentId: string, entityId: string, name: string): AsyncIterable<string>;
85
98
  /** Typed calls through a component class's handler table: `client.of(ShoppingCartEntity, cartId).call(ShoppingCartEntity.handlers.addItem).invoke(item)`. */
86
99
  of<C>(component: ComponentRef & {
87
100
  new (): C;
package/dist/client.js CHANGED
@@ -12,6 +12,7 @@ import { decodeJsonValue, reviver } from "./json.js";
12
12
  import { errorCodeFromProto, kindToProto } from "./kinds.js";
13
13
  import { metadataToProto } from "./context.js";
14
14
  import { encodePayload, decodePayload, EMPTY_PAYLOAD } from "./server/payloads.js";
15
+ import { AutonomousAgentCalls, TaskCalls, Tasks } from "./autonomous.js";
15
16
  function stubOf(connection) {
16
17
  if (!connection.stub) {
17
18
  connection.transport = createGrpcTransport({ baseUrl: `http://${connection.address}` });
@@ -219,6 +220,54 @@ export class ComponentClient {
219
220
  forAgent(componentId, sessionId) {
220
221
  return new Calls(this.#connection, this.#metadata, "agent", componentId, sessionId);
221
222
  }
223
+ /** Creates tasks for autonomous agents. */
224
+ get tasks() {
225
+ return new Tasks(this);
226
+ }
227
+ /** Calls about one task. */
228
+ forTask(taskId) {
229
+ return new TaskCalls(this, taskId);
230
+ }
231
+ /** One instance of an autonomous agent, by the id the caller chose; without one, only `runSingleTask`, on an instance the platform names. */
232
+ forAutonomousAgent(agent, instanceId) {
233
+ return new AutonomousAgentCalls(this, typeof agent === "string" ? agent : agent.componentId, instanceId);
234
+ }
235
+ /** @internal A call with a JSON body and a JSON reply, as the platform's own components take them. */
236
+ async _raw(kind, componentId, entityId, name, body) {
237
+ const answer = await stubOf(this.#connection).invoke({
238
+ kind: kindToProto(kind),
239
+ componentId,
240
+ entityId,
241
+ name,
242
+ payload: { contentType: "application/json", manifest: "", data: body === undefined ? new Uint8Array() : new TextEncoder().encode(JSON.stringify(body)) },
243
+ metadata: metadataToProto(this.#metadata),
244
+ });
245
+ switch (answer.result.case) {
246
+ case "reply": {
247
+ const data = answer.result.value.payload?.data;
248
+ return data && data.length > 0 ? JSON.parse(new TextDecoder().decode(data), reviver) : undefined;
249
+ }
250
+ case "error":
251
+ throw new CommandError(errorOf(answer.result.value));
252
+ default:
253
+ throw new CommandError({ message: "the sidecar answered nothing", code: "INTERNAL" });
254
+ }
255
+ }
256
+ /** @internal A streaming call whose tokens are text. */
257
+ async *_rawStream(kind, componentId, entityId, name) {
258
+ const request = { kind: kindToProto(kind), componentId, entityId, name, payload: EMPTY_PAYLOAD, metadata: metadataToProto(this.#metadata) };
259
+ for await (const token of stubOf(this.#connection).invokeStream(request)) {
260
+ switch (token.token.case) {
261
+ case "text":
262
+ yield token.token.value;
263
+ break;
264
+ case "completed":
265
+ return;
266
+ case "failed":
267
+ throw new CommandError(errorOf(token.token.value));
268
+ }
269
+ }
270
+ }
222
271
  /** Typed calls through a component class's handler table: `client.of(ShoppingCartEntity, cartId).call(ShoppingCartEntity.handlers.addItem).invoke(item)`. */
223
272
  of(component, entityId) {
224
273
  return new TypedCalls(new Calls(this.#connection, this.#metadata, component.prototype._kind, component.componentId, entityId));
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { Consumer, type ConsumerClass } from "./consumer.ts";
17
17
  export { TimedAction, type TimedActionClass } from "./timedAction.ts";
18
18
  export { AgentEffects, type AgentEffect } from "./effects/agent.ts";
19
19
  export { Agent, type AgentClass } from "./agent.ts";
20
+ 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";
20
21
  export { Endpoint, type EndpointClass } from "./endpoint.ts";
21
22
  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";
22
23
  export { type CommandContext, type RequestContext, type Principal, type Caller, Query, Headers } from "./context.ts";
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ export { Consumer } from "./consumer.js";
28
28
  export { TimedAction } from "./timedAction.js";
29
29
  export { AgentEffects } from "./effects/agent.js";
30
30
  export { Agent } from "./agent.js";
31
+ export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, rejected, taskAcceptance, taskRule, taskType, } from "./autonomous.js";
31
32
  export { Endpoint } from "./endpoint.js";
32
33
  export { Acl, Callers, HttpProblem, get, post, put, patch, del, sse } from "./routes.js";
33
34
  export { Query, Headers } from "./context.js";
package/dist/kinds.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Kind as ProtoKind } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
2
2
  import { ErrorCode as ProtoErrorCode } from "./_proto/ankka/protocol/v1/payload_pb.ts";
3
3
  import { ErrorCode } from "./effects/common.ts";
4
- export type ComponentKind = "event-sourced" | "key-value" | "workflow" | "view" | "consumer" | "timed-action" | "agent";
4
+ export type ComponentKind = "event-sourced" | "key-value" | "workflow" | "view" | "consumer" | "timed-action" | "agent" | "autonomous-agent";
5
5
  export declare function kindToProto(kind: ComponentKind): ProtoKind;
6
6
  export declare function kindFromProto(kind: ProtoKind): ComponentKind;
7
7
  export declare function errorCodeToProto(code: ErrorCode): ProtoErrorCode;
package/dist/kinds.js CHANGED
@@ -10,6 +10,7 @@ const KIND_TO_PROTO = Object.freeze({
10
10
  consumer: ProtoKind.CONSUMER,
11
11
  "timed-action": ProtoKind.TIMED_ACTION,
12
12
  agent: ProtoKind.AGENT,
13
+ "autonomous-agent": ProtoKind.AUTONOMOUS_AGENT,
13
14
  });
14
15
  export function kindToProto(kind) {
15
16
  return KIND_TO_PROTO[kind];
@@ -1,5 +1,5 @@
1
- import type { ConnectRouter } from "@connectrpc/connect";
2
- import { type GuardrailRequest, type GuardrailResult, type PlanReply, type PlanRequest, type ToolRequest, type ToolResult } from "../_proto/ankka/protocol/v1/agent_pb.ts";
1
+ import { type ConnectRouter } from "@connectrpc/connect";
2
+ import { type GuardrailRequest, type GuardrailResult, type PlanReply, type PlanRequest, type TaskResultRequest, type TaskResultVerdict, type ToolRequest, type ToolResult } from "../_proto/ankka/protocol/v1/agent_pb.ts";
3
3
  import type { AgentEffect } from "../effects/agent.ts";
4
4
  import type { Agent } from "../agent.ts";
5
5
  import type { RegisteredAgent } from "../service.ts";
@@ -11,4 +11,10 @@ export declare function handlePlan(req: PlanRequest, ctx: ServerContext): Promis
11
11
  export declare function runTool(registered: RegisteredAgent, agent: Agent, name: string, argumentsJson: string): Promise<string>;
12
12
  export declare function handleTool(req: ToolRequest, ctx: ServerContext): Promise<ToolResult>;
13
13
  export declare function handleGuardrail(req: GuardrailRequest, ctx: ServerContext): Promise<GuardrailResult>;
14
+ /**
15
+ * An autonomous agent's result: decoded as its task type's, then put through its rules in order. A
16
+ * rule that throws is answered as an error, which the sidecar takes as a failed iteration and tries
17
+ * again — never as a verdict, since nothing was decided.
18
+ */
19
+ export declare function handleTaskResult(req: TaskResultRequest, ctx: ServerContext): Promise<TaskResultVerdict>;
14
20
  export declare function agentRoutes(router: ConnectRouter, ctx: ServerContext): void;
@@ -1,13 +1,15 @@
1
1
  // The agent conversation: the sidecar asks this process to plan (an AgentEffect as data), to run a tool
2
2
  // with the model's arguments, and to check a guardrail. The loop, the memory and the model's key are
3
3
  // the sidecar's.
4
+ import { Code, ConnectError } from "@connectrpc/connect";
4
5
  import { create } from "@bufbuild/protobuf";
5
- import { Agent as AgentService, AgentPlan_Memory, GuardrailRequest_Stage, GuardrailResultSchema, PlanReplySchema, ToolResultSchema, } from "../_proto/ankka/protocol/v1/agent_pb.js";
6
+ import { Agent as AgentService, AgentPlan_Memory, GuardrailRequest_Stage, GuardrailResultSchema, PlanReplySchema, TaskResultVerdictSchema, ToolResultSchema, } from "../_proto/ankka/protocol/v1/agent_pb.js";
6
7
  import { codecFor, isCodec } from "../codec.js";
7
8
  import { metadataFromProto } from "../context.js";
8
9
  import { ErrorCode } from "../effects/common.js";
9
10
  import { decodeJsonValue, reviver } from "../json.js";
10
11
  import { errorCodeToProto } from "../kinds.js";
12
+ import { decodeResult } from "../autonomous.js";
11
13
  import { decodePayload } from "./payloads.js";
12
14
  function messageOf(e) {
13
15
  return e instanceof Error ? e.message : String(e);
@@ -75,6 +77,9 @@ export async function runTool(registered, agent, name, argumentsJson) {
75
77
  return typeof result === "string" ? result : JSON.stringify(result, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
76
78
  }
77
79
  export async function handleTool(req, ctx) {
80
+ const autonomous = ctx.registry.of("autonomous-agent", req.componentId);
81
+ if (autonomous)
82
+ return handleAutonomousTool(autonomous, req, ctx);
78
83
  const registered = ctx.registry.of("agent", req.componentId);
79
84
  if (!registered)
80
85
  return create(ToolResultSchema, { result: { case: "error", value: `no agent ${JSON.stringify(req.componentId)} is registered` } });
@@ -89,7 +94,7 @@ export async function handleTool(req, ctx) {
89
94
  }
90
95
  }
91
96
  export async function handleGuardrail(req, ctx) {
92
- const registered = ctx.registry.of("agent", req.componentId);
97
+ const registered = ctx.registry.of("agent", req.componentId) ?? ctx.registry.of("autonomous-agent", req.componentId);
93
98
  const guardrail = registered?.guardrails.get(req.guardrail);
94
99
  if (!registered || !guardrail)
95
100
  return create(GuardrailResultSchema, { result: { case: "block", value: `no guardrail ${JSON.stringify(req.guardrail)} on ${req.componentId}` } });
@@ -104,10 +109,62 @@ export async function handleGuardrail(req, ctx) {
104
109
  return create(GuardrailResultSchema, { result: { case: "block", value: `guardrail ${req.guardrail} failed: ${messageOf(e)}` } });
105
110
  }
106
111
  }
112
+ async function handleAutonomousTool(registered, req, ctx) {
113
+ const tool = registered.tools.get(req.tool);
114
+ if (!tool)
115
+ return create(ToolResultSchema, { result: { case: "error", value: `no tool ${JSON.stringify(req.tool)} on ${registered.id}` } });
116
+ const agent = new registered.cls();
117
+ agent._bind(req.sessionId, ctx.client);
118
+ try {
119
+ let input;
120
+ if (isCodec(tool.input))
121
+ input = tool.input.decode(new TextEncoder().encode(req.argumentsJson));
122
+ else
123
+ input = decodeJsonValue(tool.input, JSON.parse(req.argumentsJson || "{}", reviver));
124
+ const result = await tool.run(agent, input);
125
+ const text = typeof result === "string" ? result : JSON.stringify(result, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
126
+ return create(ToolResultSchema, { result: { case: "ok", value: text } });
127
+ }
128
+ catch (e) {
129
+ return create(ToolResultSchema, { result: { case: "error", value: messageOf(e) } });
130
+ }
131
+ }
132
+ /**
133
+ * An autonomous agent's result: decoded as its task type's, then put through its rules in order. A
134
+ * rule that throws is answered as an error, which the sidecar takes as a failed iteration and tries
135
+ * again — never as a verdict, since nothing was decided.
136
+ */
137
+ export async function handleTaskResult(req, ctx) {
138
+ const registered = ctx.registry.of("autonomous-agent", req.componentId);
139
+ const type = registered?.taskTypes.get(req.taskType);
140
+ if (!registered || !type)
141
+ throw new ConnectError(`no task type ${JSON.stringify(req.taskType)} on ${req.componentId}`, Code.NotFound);
142
+ let result;
143
+ try {
144
+ result = decodeResult(type, req.resultJson);
145
+ }
146
+ catch (e) {
147
+ return create(TaskResultVerdictSchema, { verdict: { case: "malformed", value: messageOf(e) } });
148
+ }
149
+ for (const rule of type.rules) {
150
+ let verdict;
151
+ try {
152
+ verdict = await rule.check(result);
153
+ }
154
+ catch (e) {
155
+ ctx.log(`ankka: rule ${registered.id}/${req.taskType}/${rule.name} threw: ${messageOf(e)}`);
156
+ throw new ConnectError(`rule ${rule.name} threw: ${messageOf(e)}`, Code.Internal);
157
+ }
158
+ if (verdict.verdict === "rejected")
159
+ return create(TaskResultVerdictSchema, { verdict: { case: "reject", value: { rule: rule.name, reason: verdict.reason } } });
160
+ }
161
+ return create(TaskResultVerdictSchema, { verdict: { case: "accept", value: {} } });
162
+ }
107
163
  export function agentRoutes(router, ctx) {
108
164
  router.service(AgentService, {
109
165
  plan: (req) => handlePlan(req, ctx),
110
166
  invokeTool: (req) => handleTool(req, ctx),
111
167
  checkGuardrail: (req) => handleGuardrail(req, ctx),
168
+ checkTaskResult: (req) => handleTaskResult(req, ctx),
112
169
  });
113
170
  }
package/dist/service.d.ts CHANGED
@@ -6,6 +6,7 @@ import { View, type ViewClass } from "./view.ts";
6
6
  import { Consumer, type ConsumerClass } from "./consumer.ts";
7
7
  import { TimedAction, type TimedActionClass } from "./timedAction.ts";
8
8
  import { Agent, type AgentClass } from "./agent.ts";
9
+ import { AutonomousAgent, type AutonomousAgentClass, type TaskType } from "./autonomous.ts";
9
10
  import { Endpoint, type EndpointClass } from "./endpoint.ts";
10
11
  import type { GuardrailRef, HandlerRef, ToolRef } from "./handlers.ts";
11
12
  import type { RouteRef, Acl } from "./routes.ts";
@@ -82,7 +83,15 @@ export interface RegisteredAgent {
82
83
  readonly role: string;
83
84
  readonly maxToolCallSteps: number;
84
85
  }
85
- export type RegisteredComponent = RegisteredEventSourced | RegisteredKeyValue | RegisteredWorkflow | RegisteredView | RegisteredConsumer | RegisteredTimedAction | RegisteredAgent;
86
+ export interface RegisteredAutonomousAgent {
87
+ readonly kind: "autonomous-agent";
88
+ readonly id: string;
89
+ readonly cls: AutonomousAgentClass<any>;
90
+ readonly tools: ReadonlyMap<string, ToolRef<any, any>>;
91
+ readonly guardrails: ReadonlyMap<string, GuardrailRef>;
92
+ readonly taskTypes: ReadonlyMap<string, TaskType<any>>;
93
+ }
94
+ export type RegisteredComponent = RegisteredEventSourced | RegisteredKeyValue | RegisteredWorkflow | RegisteredView | RegisteredConsumer | RegisteredTimedAction | RegisteredAgent | RegisteredAutonomousAgent;
86
95
  export interface RegisteredEndpoint {
87
96
  readonly id: string;
88
97
  readonly cls: EndpointClass<any>;
@@ -127,6 +136,7 @@ export declare class ServiceBuilder {
127
136
  register<M, Out, C extends Consumer<M, Out>>(cls: ConsumerClass<M, Out, C>): this;
128
137
  register<C extends TimedAction>(cls: TimedActionClass<C>): this;
129
138
  register<C extends Agent>(cls: AgentClass<C>): this;
139
+ register<C extends AutonomousAgent>(cls: AutonomousAgentClass<C>): this;
130
140
  register<C extends Endpoint>(cls: EndpointClass<C>): this;
131
141
  /** Validates every registration and returns the registry, or throws a `RegistrationError` naming every problem. */
132
142
  validate(): Registry;
package/dist/service.js CHANGED
@@ -11,6 +11,7 @@ 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
16
  import { isAcl } from "./routes.js";
16
17
  import { ComponentClient } from "./client.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();
package/dist/spec.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type MessageInitShape } from "@bufbuild/protobuf";
2
2
  import { Endpoint_Acl, type CallerMatcherSchema, type Spec } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
3
3
  import type { Registry } from "./service.ts";
4
4
  import type { Acl } from "./routes.ts";
5
- export declare const PROTOCOL_VERSION = "1.1";
5
+ export declare const PROTOCOL_VERSION = "1.2";
6
6
  export declare const SDK_NAME = "ankka-typescript";
7
7
  export declare function aclToProto(acl: Acl): Endpoint_Acl;
8
8
  /** The callers a CALLERS acl names, as discovery carries them; empty for every other rule. */
package/dist/spec.js CHANGED
@@ -1,11 +1,12 @@
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
6
  import { toJsonSchema } from "./schema.js";
6
7
  import { isCodec } from "./codec.js";
7
8
  import { VERSION } from "./version.js";
8
- export const PROTOCOL_VERSION = "1.1";
9
+ export const PROTOCOL_VERSION = "1.2";
9
10
  export const SDK_NAME = "ankka-typescript";
10
11
  export function aclToProto(acl) {
11
12
  if (typeof acl === "object")
@@ -66,6 +67,8 @@ function settingsInit(s) {
66
67
  function componentInit(c) {
67
68
  const base = { kind: kindToProto(c.kind), id: c.id };
68
69
  switch (c.kind) {
70
+ case "autonomous-agent":
71
+ return autonomousInit(c);
69
72
  case "event-sourced":
70
73
  return { ...base, handlers: handlerInits(c.handlers), detail: { case: "eventSourced", value: { snapshotEvery: c.snapshotEvery } } };
71
74
  case "key-value":
@@ -100,6 +103,40 @@ function componentInit(c) {
100
103
  };
101
104
  }
102
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
+ }
103
140
  export function renderSpec(registry) {
104
141
  const components = [...registry.components.values()].sort((a, b) => a.id.localeCompare(b.id)).map(componentInit);
105
142
  const endpoints = [...registry.endpoints.values()].map((e) => ({
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.8.0";
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.8.0";
2
+ export const VERSION = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ankka",
3
- "version": "0.8.0",
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",