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.
- 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/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 +24 -2
- package/dist/consumer.js +20 -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/endpoint.d.ts +10 -1
- package/dist/endpoint.js +20 -0
- package/dist/graph.d.ts +15 -1
- package/dist/graph.js +20 -0
- package/dist/handlers.d.ts +10 -2
- package/dist/handlers.js +6 -2
- package/dist/index.d.ts +12 -4
- package/dist/index.js +11 -3
- 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 +38 -0
- package/dist/server/workflow.js +5 -1
- package/dist/service.d.ts +40 -2
- package/dist/service.js +209 -14
- 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 +43 -2
- package/dist/testkit/kinds.js +196 -26
- 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/testkit/kinds.d.ts
CHANGED
|
@@ -5,12 +5,16 @@ import type { ConsumerEffect, TimedActionEffect, ViewEffect } from "../effects/s
|
|
|
5
5
|
import type { KeyValueEntity, KeyValueEntityClass } from "../keyValueEntity.ts";
|
|
6
6
|
import type { Workflow, WorkflowClass } from "../workflow.ts";
|
|
7
7
|
import type { View, ViewClass } from "../view.ts";
|
|
8
|
+
import type { KeyedView, KeyedViewClass } from "../keyedView.ts";
|
|
9
|
+
import { type KeyedViewEffect } from "../effects/keyed.ts";
|
|
10
|
+
import type { ComponentRef } from "../client.ts";
|
|
8
11
|
import { type Consumer, type ConsumerClass } from "../consumer.ts";
|
|
9
12
|
import { type Delta, type GraphConsumerClass } from "../graph.ts";
|
|
10
13
|
import type { TimedAction, TimedActionClass } from "../timedAction.ts";
|
|
11
14
|
import type { Agent, AgentClass } from "../agent.ts";
|
|
12
15
|
import type { HandlerRef } from "../handlers.ts";
|
|
13
16
|
import { type MaterialisedKeyValue, type MaterialisedStep, type MaterialisedWorkflowCommand } from "../materialise.ts";
|
|
17
|
+
import { type AgentOutcome, type DecisionInput } from "../approvals.ts";
|
|
14
18
|
/** Drives one key value entity instance through its handlers. */
|
|
15
19
|
export declare class KeyValueTestKit<S, C extends KeyValueEntity<S>> {
|
|
16
20
|
#private;
|
|
@@ -60,6 +64,28 @@ export declare class ViewTestKit<E, Row, C extends View<E, Row>> {
|
|
|
60
64
|
onDelete(key: string, metadata?: Metadata): Promise<ViewEffect<Row>>;
|
|
61
65
|
get(key: string): Row | null;
|
|
62
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Drives one keyed view with no sidecar: a change of one of its sources goes to that source's handler,
|
|
69
|
+
* and the rows it names are written to `rows`, a later change to a key winning, as the runtime writes
|
|
70
|
+
* them. Rows round-trip through the view's row codec and events through the source's.
|
|
71
|
+
*
|
|
72
|
+
* A declared query is SQL and there is no database to run it, so the test says what each query
|
|
73
|
+
* answers. A handler that asks a query the test has not answered throws, naming it: a kit that answered
|
|
74
|
+
* no rows would pass a handler that updated nothing.
|
|
75
|
+
*/
|
|
76
|
+
export declare class KeyedViewTestKit<Row, C extends KeyedView<Row>> {
|
|
77
|
+
#private;
|
|
78
|
+
readonly rows: Map<string, Row>;
|
|
79
|
+
private constructor();
|
|
80
|
+
static of<Row, C extends KeyedView<Row>>(cls: KeyedViewClass<Row, C>, client?: ComponentClient): KeyedViewTestKit<Row, C>;
|
|
81
|
+
/** What the declared query `name` answers, given the values it is asked with. */
|
|
82
|
+
answering(name: string, answer: (values: Readonly<Record<string, string>>) => Row[]): this;
|
|
83
|
+
/** A change of the entity `key` of `entity`, round-tripped through that source's codec. */
|
|
84
|
+
change<E>(entity: ComponentRef, key: string, event: E): Promise<KeyedViewEffect<Row>>;
|
|
85
|
+
/** The entity `key` of `entity` was deleted. */
|
|
86
|
+
deleted(entity: ComponentRef, key: string): Promise<KeyedViewEffect<Row>>;
|
|
87
|
+
get(key: string): Row | null;
|
|
88
|
+
}
|
|
63
89
|
/** Feeds a consumer messages and collects what it produced. */
|
|
64
90
|
export declare class ConsumerTestKit<M, Out, C extends Consumer<M, Out>> {
|
|
65
91
|
#private;
|
|
@@ -141,9 +167,15 @@ export declare class ScriptedModel {
|
|
|
141
167
|
/** @internal */
|
|
142
168
|
_next(call: ModelCall): ModelResponse;
|
|
143
169
|
}
|
|
170
|
+
/** Scripted MCP servers: `{ server: { tool: (args) => text } }`, called by the model as `mcp__<server>__<tool>`. */
|
|
171
|
+
export type ScriptedMcp = Readonly<Record<string, Readonly<Record<string, (args: Record<string, unknown>) => string | Promise<string>>>>>;
|
|
144
172
|
/**
|
|
145
173
|
* Runs an agent's turn the way the sidecar does, in process: the plan, input guardrails, the model
|
|
146
174
|
* (scripted), tool calls back into the agent, output guardrails, and the session's history.
|
|
175
|
+
*
|
|
176
|
+
* `mcp` scripts the agent's MCP servers, and its result guardrails check what they answer. A call to a
|
|
177
|
+
* tool that requires approval — the agent's own, or any of a server listed with `approval` — is not run:
|
|
178
|
+
* `outcome` answers with its request, `ask` rejects with `ApprovalAwaited`, and `decide` goes on.
|
|
147
179
|
*/
|
|
148
180
|
export declare class AgentTestKit<C extends Agent> {
|
|
149
181
|
#private;
|
|
@@ -154,9 +186,18 @@ export declare class AgentTestKit<C extends Agent> {
|
|
|
154
186
|
text: string;
|
|
155
187
|
}[];
|
|
156
188
|
private constructor();
|
|
157
|
-
static of<C extends Agent>(cls: AgentClass<C>, sessionId: string, model: ScriptedModel, client?: ComponentClient
|
|
158
|
-
|
|
189
|
+
static of<C extends Agent>(cls: AgentClass<C>, sessionId: string, model: ScriptedModel, client?: ComponentClient, options?: {
|
|
190
|
+
readonly mcp?: ScriptedMcp;
|
|
191
|
+
}): AgentTestKit<C>;
|
|
192
|
+
/** One turn: returns the reply text (JSON text for a `thenReplyJson` plan). A refusal or a blocked guardrail rejects with `CommandError`; a turn that waits, with `ApprovalAwaited`. */
|
|
159
193
|
ask<I>(handler: HandlerRef<C, I, any, any> | string, input?: I): Promise<string>;
|
|
194
|
+
/** One turn, answered with its outcome: the reply text, or the approval requests the turn waits on. */
|
|
195
|
+
outcome<I>(handler: HandlerRef<C, I, any, any> | string, input?: I): Promise<AgentOutcome<string>>;
|
|
196
|
+
/**
|
|
197
|
+
* Decides the request the turn waits on, and goes on as the sidecar would: approved, the tool runs and the
|
|
198
|
+
* model is told its result; refused, the model is told who refused it and why.
|
|
199
|
+
*/
|
|
200
|
+
decide(approvalId: string, decision: DecisionInput): Promise<AgentOutcome<string>>;
|
|
160
201
|
/** A streaming handler's turn, token by token: the reply split on spaces, as a model streams words. */
|
|
161
202
|
stream<I>(handler: HandlerRef<C, I, any, any> | string, input?: I): AsyncIterable<string>;
|
|
162
203
|
}
|
package/dist/testkit/kinds.js
CHANGED
|
@@ -5,12 +5,15 @@ import { ComponentClient, noClient } from "../client.js";
|
|
|
5
5
|
import { binaryCodecs, codecFor } from "../codec.js";
|
|
6
6
|
import { commandContext } from "../context.js";
|
|
7
7
|
import { CommandError, ErrorCode } from "../effects/common.js";
|
|
8
|
+
import { reduceRowChanges } from "../effects/keyed.js";
|
|
8
9
|
import { PROTOCOL_KEY, requireSeveralMessages } from "../consumer.js";
|
|
9
10
|
import { deltaRecords, readDelta } from "../graph.js";
|
|
10
11
|
import { PROTOCOL_VERSION } from "../spec.js";
|
|
11
12
|
import { materialiseKeyValue, materialiseStep, materialiseWorkflowCommand } from "../materialise.js";
|
|
12
13
|
import { Ankka } from "../service.js";
|
|
13
14
|
import { checkPlan, runTool } from "../server/agent.js";
|
|
15
|
+
import { ApprovalAwaited } from "../approvals.js";
|
|
16
|
+
import { TOOL_PREFIX, toolName } from "../mcp.js";
|
|
14
17
|
function roundTrip(codec, value) {
|
|
15
18
|
return codec.decode(codec.encode(value));
|
|
16
19
|
}
|
|
@@ -227,6 +230,86 @@ export class ViewTestKit {
|
|
|
227
230
|
return this.rows.get(key) ?? null;
|
|
228
231
|
}
|
|
229
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Drives one keyed view with no sidecar: a change of one of its sources goes to that source's handler,
|
|
235
|
+
* and the rows it names are written to `rows`, a later change to a key winning, as the runtime writes
|
|
236
|
+
* them. Rows round-trip through the view's row codec and events through the source's.
|
|
237
|
+
*
|
|
238
|
+
* A declared query is SQL and there is no database to run it, so the test says what each query
|
|
239
|
+
* answers. A handler that asks a query the test has not answered throws, naming it: a kit that answered
|
|
240
|
+
* no rows would pass a handler that updated nothing.
|
|
241
|
+
*/
|
|
242
|
+
export class KeyedViewTestKit {
|
|
243
|
+
#registered;
|
|
244
|
+
#cls;
|
|
245
|
+
#client;
|
|
246
|
+
#answers = new Map();
|
|
247
|
+
#sequence = 0;
|
|
248
|
+
rows = new Map();
|
|
249
|
+
constructor(cls, client) {
|
|
250
|
+
this.#registered = registryFor(cls, client).component(cls.componentId);
|
|
251
|
+
this.#cls = cls;
|
|
252
|
+
this.#client = client;
|
|
253
|
+
}
|
|
254
|
+
static of(cls, client = noClient()) {
|
|
255
|
+
return new KeyedViewTestKit(cls, client);
|
|
256
|
+
}
|
|
257
|
+
/** What the declared query `name` answers, given the values it is asked with. */
|
|
258
|
+
answering(name, answer) {
|
|
259
|
+
this.#answers.set(name, answer);
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
#source(entity) {
|
|
263
|
+
const source = this.#registered.sources.get(entity.componentId);
|
|
264
|
+
if (!source)
|
|
265
|
+
throw new Error(`${this.#registered.id} reads no ${JSON.stringify(entity.componentId)}`);
|
|
266
|
+
return source;
|
|
267
|
+
}
|
|
268
|
+
async #apply(key, run) {
|
|
269
|
+
const view = new this.#cls();
|
|
270
|
+
const rowCodec = this.#registered.rowCodec;
|
|
271
|
+
const id = this.#registered.id;
|
|
272
|
+
const rows = {
|
|
273
|
+
get: async (k) => {
|
|
274
|
+
const row = this.rows.get(k);
|
|
275
|
+
return row === undefined ? null : roundTrip(rowCodec, row);
|
|
276
|
+
},
|
|
277
|
+
ask: async (name, values = {}) => {
|
|
278
|
+
const answer = this.#answers.get(name);
|
|
279
|
+
if (!answer)
|
|
280
|
+
throw new Error(`${id} asked the query ${JSON.stringify(name)}, and the test has not said what it answers; say so with answering(${JSON.stringify(name)}, ...)`);
|
|
281
|
+
return answer(values).map((r) => roundTrip(rowCodec, r));
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
this.#sequence += 1;
|
|
285
|
+
const metadata = { "ce-subject": key, "ankka.sequence": String(this.#sequence) };
|
|
286
|
+
view._bind(metadata, this.#client.withMetadata(metadata), rows);
|
|
287
|
+
const effect = ((await run(view)) ?? { kind: "rows", changes: [] });
|
|
288
|
+
if (effect.kind !== "rows")
|
|
289
|
+
throw new TypeError(`${id}'s handler returned something that is not a keyed view effect`);
|
|
290
|
+
for (const [k, row] of reduceRowChanges(effect.changes)) {
|
|
291
|
+
if (row === undefined)
|
|
292
|
+
this.rows.delete(k);
|
|
293
|
+
else
|
|
294
|
+
this.rows.set(k, roundTrip(rowCodec, row));
|
|
295
|
+
}
|
|
296
|
+
return effect;
|
|
297
|
+
}
|
|
298
|
+
/** A change of the entity `key` of `entity`, round-tripped through that source's codec. */
|
|
299
|
+
change(entity, key, event) {
|
|
300
|
+
const source = this.#source(entity);
|
|
301
|
+
const wire = roundTrip(source.eventCodec, event);
|
|
302
|
+
return this.#apply(key, (v) => source.onChange(v, wire));
|
|
303
|
+
}
|
|
304
|
+
/** The entity `key` of `entity` was deleted. */
|
|
305
|
+
deleted(entity, key) {
|
|
306
|
+
const source = this.#source(entity);
|
|
307
|
+
return this.#apply(key, (v) => (source.deleted ? source.deleted(v) : undefined));
|
|
308
|
+
}
|
|
309
|
+
get(key) {
|
|
310
|
+
return this.rows.get(key) ?? null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
230
313
|
/** Feeds a consumer messages and collects what it produced. */
|
|
231
314
|
export class ConsumerTestKit {
|
|
232
315
|
#registered;
|
|
@@ -372,32 +455,50 @@ export class ScriptedModel {
|
|
|
372
455
|
/**
|
|
373
456
|
* Runs an agent's turn the way the sidecar does, in process: the plan, input guardrails, the model
|
|
374
457
|
* (scripted), tool calls back into the agent, output guardrails, and the session's history.
|
|
458
|
+
*
|
|
459
|
+
* `mcp` scripts the agent's MCP servers, and its result guardrails check what they answer. A call to a
|
|
460
|
+
* tool that requires approval — the agent's own, or any of a server listed with `approval` — is not run:
|
|
461
|
+
* `outcome` answers with its request, `ask` rejects with `ApprovalAwaited`, and `decide` goes on.
|
|
375
462
|
*/
|
|
376
463
|
export class AgentTestKit {
|
|
377
464
|
#registered;
|
|
378
465
|
#cls;
|
|
379
466
|
#client;
|
|
380
467
|
#model;
|
|
468
|
+
#mcp;
|
|
381
469
|
sessionId;
|
|
382
470
|
/** The session's memory: what the user said and the agent answered, turn after turn. */
|
|
383
471
|
history = [];
|
|
384
|
-
|
|
472
|
+
#turn;
|
|
473
|
+
#decided = new Set();
|
|
474
|
+
#nextId = 0;
|
|
475
|
+
constructor(cls, sessionId, model, client, mcp) {
|
|
385
476
|
this.#registered = registryFor(cls, client).component(cls.componentId);
|
|
386
477
|
this.#cls = cls;
|
|
387
478
|
this.#client = client;
|
|
388
479
|
this.#model = model;
|
|
480
|
+
this.#mcp = mcp;
|
|
389
481
|
this.sessionId = sessionId;
|
|
390
482
|
}
|
|
391
|
-
static of(cls, sessionId, model, client = noClient()) {
|
|
392
|
-
return new AgentTestKit(cls, sessionId, model, client);
|
|
483
|
+
static of(cls, sessionId, model, client = noClient(), options = {}) {
|
|
484
|
+
return new AgentTestKit(cls, sessionId, model, client, options.mcp ?? {});
|
|
393
485
|
}
|
|
394
486
|
#agent() {
|
|
395
487
|
const a = new this.#cls();
|
|
396
488
|
a._bind(this.sessionId, {}, this.#client);
|
|
397
489
|
return a;
|
|
398
490
|
}
|
|
399
|
-
/** One turn: returns the reply text (JSON text for a `thenReplyJson` plan). A refusal or a blocked guardrail rejects with `CommandError`. */
|
|
491
|
+
/** One turn: returns the reply text (JSON text for a `thenReplyJson` plan). A refusal or a blocked guardrail rejects with `CommandError`; a turn that waits, with `ApprovalAwaited`. */
|
|
400
492
|
async ask(handler, input) {
|
|
493
|
+
const outcome = await this.outcome(handler, input);
|
|
494
|
+
if (outcome.kind === "awaiting-approval")
|
|
495
|
+
throw new ApprovalAwaited(outcome.requests);
|
|
496
|
+
return outcome.value;
|
|
497
|
+
}
|
|
498
|
+
/** One turn, answered with its outcome: the reply text, or the approval requests the turn waits on. */
|
|
499
|
+
async outcome(handler, input) {
|
|
500
|
+
if (this.#turn?.pending)
|
|
501
|
+
throw new CommandError({ message: `session '${this.sessionId}' has a turn awaiting a decision`, code: ErrorCode.Conflict });
|
|
401
502
|
const ref = findHandler(this.#registered.handlers, handler, this.#registered.cls.name);
|
|
402
503
|
const wireInput = ref.input ? roundTrip(codecFor(ref.input), input) : undefined;
|
|
403
504
|
const agent = this.#agent();
|
|
@@ -409,43 +510,112 @@ export class AgentTestKit {
|
|
|
409
510
|
throw new Error(problem);
|
|
410
511
|
if (plan.failure)
|
|
411
512
|
throw new CommandError(plan.failure);
|
|
412
|
-
const guard = async (stage, text) => {
|
|
413
|
-
for (const name of plan.guardrailNames) {
|
|
414
|
-
const reason = await this.#registered.guardrails.get(name).check(stage, text);
|
|
415
|
-
if (reason)
|
|
416
|
-
throw new CommandError({ message: `${name}: ${reason}`, code: ErrorCode.Forbidden });
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
513
|
const user = [...plan.context, plan.user ?? ""].filter((t) => t !== "").join("\n\n");
|
|
420
|
-
await guard("input", user);
|
|
514
|
+
await this.#guard(plan, "input", user);
|
|
421
515
|
const messages = plan.sessionMemory ? this.history.map((h) => ({ ...h })) : [];
|
|
422
516
|
messages.push({ role: "user", text: user });
|
|
423
|
-
|
|
424
|
-
|
|
517
|
+
this.#turn = { plan, messages, steps: 0 };
|
|
518
|
+
return this.#goOn(this.#turn);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Decides the request the turn waits on, and goes on as the sidecar would: approved, the tool runs and the
|
|
522
|
+
* model is told its result; refused, the model is told who refused it and why.
|
|
523
|
+
*/
|
|
524
|
+
async decide(approvalId, decision) {
|
|
525
|
+
if (decision.by.trim() === "")
|
|
526
|
+
throw new CommandError({ message: "a decision must name who made it", code: ErrorCode.BadRequest });
|
|
527
|
+
if (this.#decided.has(approvalId))
|
|
528
|
+
throw new CommandError({ message: `approval request '${approvalId}' is decided`, code: ErrorCode.Conflict });
|
|
529
|
+
const turn = this.#turn;
|
|
530
|
+
if (!turn?.pending || turn.pending.request.id !== approvalId) {
|
|
531
|
+
throw new CommandError({ message: `no approval request '${approvalId}' in session '${this.sessionId}'`, code: ErrorCode.NotFound });
|
|
532
|
+
}
|
|
533
|
+
const { tool, argumentsJson } = turn.pending;
|
|
534
|
+
turn.pending = undefined;
|
|
535
|
+
this.#decided.add(approvalId);
|
|
536
|
+
const result = decision.approved
|
|
537
|
+
? await this.#run(tool, argumentsJson)
|
|
538
|
+
: `A person (${decision.by}) refused this tool call; the tool did not run.${decision.note ? ` Their note: ${decision.note}` : ""}`;
|
|
539
|
+
turn.messages.push({ role: "tool", text: result });
|
|
540
|
+
return this.#goOn(turn);
|
|
541
|
+
}
|
|
542
|
+
async #guard(plan, stage, text) {
|
|
543
|
+
for (const name of plan.guardrailNames) {
|
|
544
|
+
const reason = await this.#registered.guardrails.get(name).check(stage, text);
|
|
545
|
+
if (reason)
|
|
546
|
+
throw new CommandError({ message: `${name}: ${reason}`, code: ErrorCode.Forbidden });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
#mcpTool(name) {
|
|
550
|
+
if (!name.startsWith(TOOL_PREFIX))
|
|
551
|
+
return undefined;
|
|
552
|
+
const rest = name.slice(TOOL_PREFIX.length);
|
|
553
|
+
const at = rest.indexOf("__");
|
|
554
|
+
return at < 0 ? undefined : { server: rest.slice(0, at), tool: rest.slice(at + 2) };
|
|
555
|
+
}
|
|
556
|
+
#offered(plan) {
|
|
557
|
+
const mcp = [...this.#registered.mcpServers.keys()].flatMap((server) => Object.keys(this.#mcp[server] ?? {}).map((tool) => toolName(server, tool)));
|
|
558
|
+
return [...plan.toolNames, ...mcp];
|
|
559
|
+
}
|
|
560
|
+
#requiresApproval(name) {
|
|
561
|
+
const mcp = this.#mcpTool(name);
|
|
562
|
+
if (mcp)
|
|
563
|
+
return this.#registered.mcpServers.get(mcp.server)?.approval !== undefined;
|
|
564
|
+
return this.#registered.tools.get(name)?.approval !== undefined;
|
|
565
|
+
}
|
|
566
|
+
async #run(name, argumentsJson) {
|
|
567
|
+
const mcp = this.#mcpTool(name);
|
|
568
|
+
try {
|
|
569
|
+
if (!mcp)
|
|
570
|
+
return await runTool(this.#registered, this.#agent(), name, argumentsJson);
|
|
571
|
+
const text = await this.#mcp[mcp.server][mcp.tool](JSON.parse(argumentsJson || "{}"));
|
|
572
|
+
for (const g of [...this.#registered.resultGuardrails.values()].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
573
|
+
const reason = await g.check(name, text);
|
|
574
|
+
if (reason)
|
|
575
|
+
return `A result guardrail refused what this tool answered, so it is not shown: ${g.name}: ${reason}`;
|
|
576
|
+
}
|
|
577
|
+
return text;
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
return `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async #goOn(turn) {
|
|
584
|
+
const offered = this.#offered(turn.plan);
|
|
585
|
+
while (turn.steps <= this.#registered.maxToolCallSteps) {
|
|
586
|
+
const answer = this.#model._next({ system: turn.plan.system, messages: [...turn.messages], tools: offered });
|
|
425
587
|
switch (answer.kind) {
|
|
426
588
|
case "refusal":
|
|
589
|
+
this.#turn = undefined;
|
|
427
590
|
throw new CommandError({ message: answer.message, code: ErrorCode.BadRequest });
|
|
428
591
|
case "tool-call": {
|
|
429
|
-
|
|
592
|
+
turn.steps++;
|
|
593
|
+
if (!offered.includes(answer.tool))
|
|
430
594
|
throw new Error(`the model called ${JSON.stringify(answer.tool)}, which the plan did not offer`);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
595
|
+
turn.messages.push({ role: "assistant", text: `[tool call ${answer.tool} ${answer.argumentsJson}]` });
|
|
596
|
+
if (this.#requiresApproval(answer.tool)) {
|
|
597
|
+
const request = Object.freeze({
|
|
598
|
+
id: `approval-${++this.#nextId}`,
|
|
599
|
+
tool: answer.tool,
|
|
600
|
+
arguments: JSON.parse(answer.argumentsJson || "{}"),
|
|
601
|
+
requestedAt: Date.now(),
|
|
602
|
+
});
|
|
603
|
+
turn.pending = { request, tool: answer.tool, argumentsJson: answer.argumentsJson };
|
|
604
|
+
return { kind: "awaiting-approval", requests: [request] };
|
|
437
605
|
}
|
|
438
|
-
messages.push({ role: "
|
|
606
|
+
turn.messages.push({ role: "tool", text: await this.#run(answer.tool, answer.argumentsJson) });
|
|
439
607
|
break;
|
|
440
608
|
}
|
|
441
609
|
case "text": {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
610
|
+
this.#turn = undefined;
|
|
611
|
+
await this.#guard(turn.plan, "output", answer.text);
|
|
612
|
+
if (turn.plan.sessionMemory)
|
|
613
|
+
this.history.push({ role: "user", text: turn.plan.user ?? "" }, { role: "assistant", text: answer.text });
|
|
614
|
+
return { kind: "answered", value: answer.text };
|
|
446
615
|
}
|
|
447
616
|
}
|
|
448
617
|
}
|
|
618
|
+
this.#turn = undefined;
|
|
449
619
|
throw new CommandError({ message: `the agent made more than ${this.#registered.maxToolCallSteps} tool calls`, code: ErrorCode.Internal });
|
|
450
620
|
}
|
|
451
621
|
/** A streaming handler's turn, token by token: the reply split on spaces, as a model streams words. */
|
package/dist/testkit/unit.d.ts
CHANGED
|
@@ -34,15 +34,28 @@ export declare class Response {
|
|
|
34
34
|
export interface RequestOptions {
|
|
35
35
|
readonly query?: Readonly<Record<string, string | readonly string[]>>;
|
|
36
36
|
readonly headers?: Readonly<Record<string, string>>;
|
|
37
|
-
|
|
37
|
+
/** The verified caller, as the runtime would hand it over; `claims` and `issuer` may be left out. */
|
|
38
|
+
readonly principal?: Omit<Principal, "claims" | "issuer"> & Partial<Pick<Principal, "claims" | "issuer">>;
|
|
38
39
|
readonly metadata?: Metadata;
|
|
39
40
|
}
|
|
41
|
+
/** What a socket route's handler did with a scripted socket. */
|
|
42
|
+
export interface SocketRun {
|
|
43
|
+
readonly sent: readonly string[];
|
|
44
|
+
readonly ended: "finished" | "failed";
|
|
45
|
+
readonly error?: string;
|
|
46
|
+
}
|
|
40
47
|
/** Calls an endpoint's routes by path, matching them as the sidecar's router does: literal segments outrank parameters. */
|
|
41
48
|
export declare class EndpointTestKit<C extends Endpoint> {
|
|
42
49
|
#private;
|
|
43
50
|
private constructor();
|
|
44
51
|
static of<C extends Endpoint>(cls: EndpointClass<C>, client?: ComponentClient): EndpointTestKit<C>;
|
|
45
52
|
request(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<Response>;
|
|
53
|
+
/**
|
|
54
|
+
* Opens a socket route with no runtime: the handler is given `frames` one at a time and then told the
|
|
55
|
+
* socket is closed, as when the client closes it after sending them. Answers what the handler sent, in
|
|
56
|
+
* order, and how it ended: `finished` when it returned, `failed` (with `error`) when it threw.
|
|
57
|
+
*/
|
|
58
|
+
socket(path: string, frames?: readonly string[], options?: RequestOptions): Promise<SocketRun>;
|
|
46
59
|
/** The frames of an SSE route, each string one frame. */
|
|
47
60
|
sse(path: string, options?: RequestOptions): Promise<string[]>;
|
|
48
61
|
get(path: string, options?: RequestOptions): Promise<Response>;
|
package/dist/testkit/unit.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// reduction is the server's (`materialise.ts`), so a test cannot pass on something the sidecar would
|
|
5
5
|
// not do.
|
|
6
6
|
import { create } from "@bufbuild/protobuf";
|
|
7
|
-
import { HttpRequestSchema } from "../_proto/ankka/protocol/v1/endpoint_pb.js";
|
|
7
|
+
import { HttpRequestSchema, SocketInSchema } from "../_proto/ankka/protocol/v1/endpoint_pb.js";
|
|
8
8
|
import { ComponentClient, noClient } from "../client.js";
|
|
9
9
|
import { binaryCodecs, codecFor } from "../codec.js";
|
|
10
10
|
import { commandContext } from "../context.js";
|
|
@@ -119,7 +119,8 @@ export class EndpointTestKit {
|
|
|
119
119
|
const rest = withoutQuery.startsWith(prefix) ? withoutQuery.slice(prefix.length) || "/" : withoutQuery;
|
|
120
120
|
const candidates = [];
|
|
121
121
|
for (const [id, route] of Object.entries(this.#cls.routes)) {
|
|
122
|
-
|
|
122
|
+
// A socket route is opened through `socket`, never answered as a GET.
|
|
123
|
+
if ((route.socket ? "SOCKET" : route.method) !== method.toUpperCase())
|
|
123
124
|
continue;
|
|
124
125
|
const pattern = new RegExp("^" + route.template.replace(/[.*+?^$()|[\]\\]/g, "\\$&").replace(PLACEHOLDER, "([^/]+)") + "$");
|
|
125
126
|
const m = pattern.exec(rest);
|
|
@@ -156,7 +157,15 @@ export class EndpointTestKit {
|
|
|
156
157
|
}
|
|
157
158
|
const headers = Object.entries(options.headers ?? {}).map(([name, value]) => ({ name, value }));
|
|
158
159
|
const principal = options.principal
|
|
159
|
-
? {
|
|
160
|
+
? {
|
|
161
|
+
subject: options.principal.subject,
|
|
162
|
+
name: options.principal.name ?? undefined,
|
|
163
|
+
email: options.principal.email ?? undefined,
|
|
164
|
+
emailVerified: options.principal.emailVerified,
|
|
165
|
+
roles: [...options.principal.roles],
|
|
166
|
+
claims: { ...(options.principal.claims ?? {}) },
|
|
167
|
+
issuer: options.principal.issuer ?? undefined,
|
|
168
|
+
}
|
|
160
169
|
: undefined;
|
|
161
170
|
const req = create(HttpRequestSchema, {
|
|
162
171
|
endpointId: this.#cls.name,
|
|
@@ -192,6 +201,34 @@ export class EndpointTestKit {
|
|
|
192
201
|
}
|
|
193
202
|
return new Response(500, "text/plain", new TextEncoder().encode(reply.message.case === "failure" ? reply.message.value.error?.message ?? "failure" : "no reply"));
|
|
194
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Opens a socket route with no runtime: the handler is given `frames` one at a time and then told the
|
|
206
|
+
* socket is closed, as when the client closes it after sending them. Answers what the handler sent, in
|
|
207
|
+
* order, and how it ended: `finished` when it returned, `failed` (with `error`) when it threw.
|
|
208
|
+
*/
|
|
209
|
+
async socket(path, frames = [], options = {}) {
|
|
210
|
+
const prepared = this.#request("SOCKET", path, undefined, options);
|
|
211
|
+
if (!prepared)
|
|
212
|
+
throw new Error(`no socket route at ${path}`);
|
|
213
|
+
const requests = [
|
|
214
|
+
create(SocketInSchema, { message: { case: "open", value: prepared.req } }),
|
|
215
|
+
...frames.map((text) => create(SocketInSchema, { message: { case: "frame", value: { kind: { case: "text", value: text } } } })),
|
|
216
|
+
create(SocketInSchema, { message: { case: "closed", value: { reason: "client" } } }),
|
|
217
|
+
];
|
|
218
|
+
async function* script() {
|
|
219
|
+
yield* requests;
|
|
220
|
+
}
|
|
221
|
+
const sent = [];
|
|
222
|
+
for await (const out of this.#dispatcher.handleSocket(script())) {
|
|
223
|
+
if (out.message.case === "frame" && out.message.value.kind.case === "text")
|
|
224
|
+
sent.push(out.message.value.kind.value);
|
|
225
|
+
if (out.message.case === "completed")
|
|
226
|
+
return { sent, ended: "finished" };
|
|
227
|
+
if (out.message.case === "failed")
|
|
228
|
+
return { sent, ended: "failed", error: out.message.value.message };
|
|
229
|
+
}
|
|
230
|
+
return { sent, ended: "finished" };
|
|
231
|
+
}
|
|
195
232
|
/** The frames of an SSE route, each string one frame. */
|
|
196
233
|
async sse(path, options = {}) {
|
|
197
234
|
const prepared = this.#request("GET", path, undefined, options);
|
package/dist/timedAction.d.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import type { Metadata } from "./effects/common.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type ComponentClient, type Secrets } from "./client.ts";
|
|
3
3
|
import type { HandlerTable } from "./handlers.ts";
|
|
4
4
|
import { TimedActionEffects } from "./effects/stateless.ts";
|
|
5
|
+
import { type Services } from "./services.ts";
|
|
5
6
|
export declare abstract class TimedAction {
|
|
6
7
|
#private;
|
|
7
8
|
readonly effects: TimedActionEffects;
|
|
8
|
-
/** `ankka.timer` is the timer's id, `ankka.attempts` how many times it has fired. */
|
|
9
|
+
/** `ankka.timer` is the timer's id, `ankka.attempts` how many times it has fired, `ankka.due` the due time in epoch milliseconds. */
|
|
9
10
|
get metadata(): Metadata;
|
|
11
|
+
/**
|
|
12
|
+
* The due time this run is for, from `ankka.due`: the same on every retry of one due time, and for a
|
|
13
|
+
* recurring timer a whole number of periods after the one before. `undefined` on a runtime older
|
|
14
|
+
* than protocol 1.12, which does not send it.
|
|
15
|
+
*/
|
|
16
|
+
get dueTime(): Date | undefined;
|
|
10
17
|
get client(): ComponentClient;
|
|
18
|
+
/** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
|
|
19
|
+
get secrets(): Secrets;
|
|
20
|
+
/** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
|
|
21
|
+
set secrets(store: Secrets);
|
|
22
|
+
/** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
|
|
23
|
+
get services(): Services;
|
|
24
|
+
/** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
|
|
25
|
+
set services(services: Services);
|
|
11
26
|
/** @internal */
|
|
12
27
|
get _kind(): "timed-action";
|
|
13
28
|
/** @internal */
|
package/dist/timedAction.js
CHANGED
|
@@ -2,20 +2,52 @@
|
|
|
2
2
|
// sweeper delivers it here, retrying a failure with backoff.
|
|
3
3
|
//
|
|
4
4
|
// static readonly actions = { remind: action("remind", s.string, (t: Reminder, id) => t.remind(id)) }
|
|
5
|
+
import { secretsFor } from "./client.js";
|
|
5
6
|
import { TimedActionEffects } from "./effects/stateless.js";
|
|
7
|
+
import { servicesFor } from "./services.js";
|
|
6
8
|
export class TimedAction {
|
|
7
9
|
effects = new TimedActionEffects();
|
|
8
10
|
#metadata = {};
|
|
9
11
|
#client;
|
|
10
|
-
/** `ankka.timer` is the timer's id, `ankka.attempts` how many times it has fired. */
|
|
12
|
+
/** `ankka.timer` is the timer's id, `ankka.attempts` how many times it has fired, `ankka.due` the due time in epoch milliseconds. */
|
|
11
13
|
get metadata() {
|
|
12
14
|
return this.#metadata;
|
|
13
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The due time this run is for, from `ankka.due`: the same on every retry of one due time, and for a
|
|
18
|
+
* recurring timer a whole number of periods after the one before. `undefined` on a runtime older
|
|
19
|
+
* than protocol 1.12, which does not send it.
|
|
20
|
+
*/
|
|
21
|
+
get dueTime() {
|
|
22
|
+
const raw = this.#metadata["ankka.due"];
|
|
23
|
+
if (raw === undefined || !/^-?\d+$/.test(raw.trim()))
|
|
24
|
+
return undefined;
|
|
25
|
+
const date = new Date(Number(raw.trim()));
|
|
26
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
27
|
+
}
|
|
14
28
|
get client() {
|
|
15
29
|
if (!this.#client)
|
|
16
30
|
throw new Error("client is only available inside an action");
|
|
17
31
|
return this.#client;
|
|
18
32
|
}
|
|
33
|
+
#secrets;
|
|
34
|
+
/** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
|
|
35
|
+
get secrets() {
|
|
36
|
+
return this.#secrets ?? secretsFor(this.client);
|
|
37
|
+
}
|
|
38
|
+
/** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
|
|
39
|
+
set secrets(store) {
|
|
40
|
+
this.#secrets = store;
|
|
41
|
+
}
|
|
42
|
+
#services;
|
|
43
|
+
/** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
|
|
44
|
+
get services() {
|
|
45
|
+
return this.#services ?? servicesFor(this.client);
|
|
46
|
+
}
|
|
47
|
+
/** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
|
|
48
|
+
set services(services) {
|
|
49
|
+
this.#services = services;
|
|
50
|
+
}
|
|
19
51
|
/** @internal */
|
|
20
52
|
get _kind() {
|
|
21
53
|
return "timed-action";
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.11.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.
|
|
2
|
+
export const VERSION = "0.11.0";
|
package/dist/view.d.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
+
import type { StartFrom } from "./startFrom.ts";
|
|
2
|
+
import type { Contract } from "./contract.ts";
|
|
1
3
|
import type { Shape } from "./codec.ts";
|
|
2
4
|
import type { Metadata } from "./effects/common.ts";
|
|
3
5
|
import type { ComponentClient } from "./client.ts";
|
|
4
6
|
import type { ComponentRef } from "./client.ts";
|
|
5
7
|
import { ViewEffects, type ViewEffect } from "./effects/stateless.ts";
|
|
6
8
|
type MaybePromise<T> = T | Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* A question a view can be asked by name: one SQL statement over the view's own table, whose values
|
|
11
|
+
* are the `:name`s it holds. The platform checks the statement when the service starts — one that is
|
|
12
|
+
* not a single read of the view's own table stops the service, naming the view and the query — and
|
|
13
|
+
* binds each value as a parameter, never as part of the text. The answer is rows of the view's row
|
|
14
|
+
* shape, read from the statement's `payload` column.
|
|
15
|
+
*/
|
|
16
|
+
export interface DeclaredQuery {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly statement: string;
|
|
19
|
+
}
|
|
20
|
+
/** Declares a query for a view's `static declared`. The SDK sends the statement as written. */
|
|
21
|
+
export declare function declaredQuery(name: string, statement: string): DeclaredQuery;
|
|
22
|
+
/** The table a view's rows are kept in, for a declared query's statement to name. */
|
|
23
|
+
export declare function tableOf(componentId: string): string;
|
|
7
24
|
export declare abstract class View<E, Row> {
|
|
8
25
|
#private;
|
|
9
26
|
readonly effects: ViewEffects<Row>;
|
|
@@ -29,9 +46,24 @@ export interface ViewClass<E = unknown, Row = unknown, C extends View<E, Row> =
|
|
|
29
46
|
/** The component whose changes feed the view, or `topic`. */
|
|
30
47
|
readonly source?: ComponentRef;
|
|
31
48
|
readonly topic?: string;
|
|
49
|
+
/** Where a topic source starts the first time its group reads the topic. */
|
|
50
|
+
readonly startFrom?: StartFrom;
|
|
51
|
+
/**
|
|
52
|
+
* Raised to have the view emptied and read again: a topic from its start position, under a group of
|
|
53
|
+
* its own, as far back as the broker retains; an entity from its first event or state. Absent is 1.
|
|
54
|
+
*/
|
|
55
|
+
readonly version?: number;
|
|
56
|
+
/** The contract the topic read is expected to carry, checked at start against the project's declaration. */
|
|
57
|
+
readonly contract?: Contract;
|
|
58
|
+
/** The declared broker the topic read is on; absent is the installation's. */
|
|
59
|
+
readonly broker?: string;
|
|
60
|
+
/** Whether the partitions an instance holds are handled at once, each in order. */
|
|
61
|
+
readonly parallel?: boolean;
|
|
32
62
|
readonly events: Shape<E>;
|
|
33
63
|
readonly row: Shape<Row>;
|
|
34
64
|
/** The query names the view answers; `get` and `all` by default. */
|
|
35
65
|
readonly queries?: readonly string[];
|
|
66
|
+
/** The queries the view can be asked by name, each a statement over its own table (protocol 1.13). */
|
|
67
|
+
readonly declared?: readonly DeclaredQuery[];
|
|
36
68
|
}
|
|
37
69
|
export {};
|
package/dist/view.js
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
// onChange(event) { ... return this.effects.updateRow(...) }
|
|
10
10
|
// }
|
|
11
11
|
import { ViewEffects } from "./effects/stateless.js";
|
|
12
|
+
/** Declares a query for a view's `static declared`. The SDK sends the statement as written. */
|
|
13
|
+
export function declaredQuery(name, statement) {
|
|
14
|
+
return Object.freeze({ name, statement });
|
|
15
|
+
}
|
|
16
|
+
/** The table a view's rows are kept in, for a declared query's statement to name. */
|
|
17
|
+
export function tableOf(componentId) {
|
|
18
|
+
return "ankka_view_" + componentId.replace(/[^A-Za-z0-9]/g, "_");
|
|
19
|
+
}
|
|
12
20
|
export class View {
|
|
13
21
|
effects = new ViewEffects();
|
|
14
22
|
#row = null;
|