@xandreed/sdk 0.14.0 → 0.15.1

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/agent.adapter.d.ts +66 -34
  3. package/dist/agent.adapter.js +48 -37
  4. package/dist/conformance/index.d.ts +2 -0
  5. package/dist/conformance/index.js +1 -0
  6. package/dist/inbox-runner.adapter.d.ts +18 -0
  7. package/dist/inbox-runner.adapter.js +8 -0
  8. package/dist/index.d.ts +35 -2
  9. package/dist/index.js +59 -2
  10. package/dist/task-executor.adapter.d.ts +32 -0
  11. package/dist/task-executor.adapter.js +10 -0
  12. package/dist/task-inbox-runner.adapter.d.ts +11 -0
  13. package/dist/task-inbox-runner.adapter.js +22 -0
  14. package/dist/turn/memory-checkpoint.adapter.d.ts +17 -0
  15. package/dist/turn/memory-checkpoint.adapter.js +62 -0
  16. package/dist/turn/model-call-recorder.adapter.d.ts +9 -0
  17. package/dist/turn/model-call-recorder.adapter.js +93 -0
  18. package/dist/turn/model-call-recorder.entity.d.ts +145 -0
  19. package/dist/turn/model-call-recorder.entity.functions.d.ts +5 -0
  20. package/dist/turn/model-call-recorder.entity.functions.js +4 -0
  21. package/dist/turn/model-call-recorder.entity.js +8 -0
  22. package/dist/turn/tool-registry.conformance.d.ts +61 -0
  23. package/dist/turn/tool-registry.conformance.js +547 -0
  24. package/dist/turn/turn-after-reply.entity.functions.d.ts +9 -0
  25. package/dist/turn/turn-after-reply.entity.functions.js +4 -0
  26. package/dist/turn/turn-lifecycle.usecase.functions.d.ts +135 -0
  27. package/dist/turn/turn-lifecycle.usecase.functions.js +131 -0
  28. package/dist/turn/turn-observation.entity.d.ts +28 -0
  29. package/dist/turn/turn-observation.entity.functions.d.ts +15 -0
  30. package/dist/turn/turn-observation.entity.functions.js +17 -0
  31. package/dist/turn/turn-observation.entity.js +12 -0
  32. package/dist/turn/turn-overflow.entity.d.ts +7 -0
  33. package/dist/turn/turn-overflow.entity.functions.d.ts +11 -0
  34. package/dist/turn/turn-overflow.entity.functions.js +12 -0
  35. package/dist/turn/turn-overflow.entity.js +5 -0
  36. package/dist/turn/turn-phase.entity.d.ts +462 -0
  37. package/dist/turn/turn-phase.entity.functions.d.ts +44 -0
  38. package/dist/turn/turn-phase.entity.functions.js +49 -0
  39. package/dist/turn/turn-phase.entity.js +13 -0
  40. package/dist/turn/turn-run.usecase.functions.d.ts +72 -0
  41. package/dist/turn/turn-run.usecase.functions.js +329 -0
  42. package/dist/turn/turn.adapter.d.ts +26 -0
  43. package/dist/turn/turn.adapter.js +225 -0
  44. package/package.json +10 -4
  45. package/dist/activity.test.d.ts +0 -1
  46. package/dist/agent.adapter.test.d.ts +0 -1
  47. package/dist/agent.backends.test.d.ts +0 -1
  48. package/dist/agent.golden.test.d.ts +0 -231
  49. package/dist/agent.trace.test.d.ts +0 -1
  50. package/dist/events-guide.test.d.ts +0 -207
  51. package/dist/journal-compat.test.d.ts +0 -1
  52. package/dist/memory.conformance.test.d.ts +0 -1
  53. package/dist/models-plugin.test.d.ts +0 -1
  54. package/dist/plugin-stack.test.d.ts +0 -1
  55. package/dist/resume.journey.test.d.ts +0 -1
  56. package/dist/testing.port.d.ts +0 -16
  57. package/dist/testing.port.js +0 -7
  58. package/dist/turn-composition.test.d.ts +0 -1
@@ -0,0 +1,62 @@
1
+ import { Effect, Exit, Option, Ref, Schema, Semaphore } from "effect";
2
+ import { canonicalJson, frameworkDraft, MEMORY_KINDS, MemorySnapshotCandidate, MemorySnapshotData, SessionLogError, storedJson } from "@xandreed/core";
3
+ import { entryOfEvent, prepareMemorySnapshot } from "@xandreed/core/internal";
4
+ const invalid = (message, cause) => new SessionLogError({ code: "session.decode", message, ...cause === void 0 ? {} : { cause } }), locate = (events) => Effect.forEach(events, (event) => entryOfEvent(event).pipe(Effect.map((entry) => Option.map(entry, (entry) => ({ event, entry }))), Effect.mapError((cause) => invalid("snapshot source memory did not decode", cause))), { concurrency: 1 }).pipe(Effect.map((all) => all.flatMap(Option.toArray))), candidateJson = (candidate) => Schema.encodeEffect(MemorySnapshotCandidate)(candidate).pipe(Effect.flatMap(storedJson), Effect.mapError((cause) => invalid("memory snapshot candidate did not encode", cause)));
5
+ export const makeMemoryCheckpoint = Effect.fnUntraced(function* (writer, history) {
6
+ const gate = yield* Semaphore.make(1), known = yield* Ref.make(Option.none()), terminal = yield* Ref.make(Option.none()), located = yield* Ref.make(Option.none()), historyLocated = Ref.get(located).pipe(Effect.flatMap(Option.match({
7
+ onNone: () => history.pipe(Effect.flatMap(locate), Effect.tap((entries) => Ref.set(located, Option.some(entries)))),
8
+ onSome: Effect.succeed
9
+ }))), refresh = Effect.gen(function* () {
10
+ const previous = yield* Option.match(yield* Ref.get(known), {
11
+ onNone: () => historyLocated.pipe(Effect.map((entries) => ({ entries, cursor: writer.started.seq - 1 }))),
12
+ onSome: Effect.succeed
13
+ }), own = (yield* writer.snapshot(MEMORY_KINDS, previous.cursor)).filter((event) => event.session === writer.started.session && event.seq > previous.cursor), next = { entries: [...previous.entries, ...yield* locate(own)], cursor: own.reduce((last, event) => Math.max(last, event.seq), previous.cursor) };
14
+ yield* Ref.set(known, Option.some(next));
15
+ return next.entries;
16
+ });
17
+ return {
18
+ load: gate.withPermits(1)(Effect.gen(function* () {
19
+ const selected = writer.memorySnapshot === void 0 ? Option.none() : yield* writer.memorySnapshot;
20
+ if (Option.isNone(selected))
21
+ return Option.none();
22
+ const row = selected.value;
23
+ if (row.kind !== "memory.snapshot")
24
+ return yield* Effect.fail(invalid("selected memory snapshot has the wrong kind"));
25
+ if (row.session === writer.started.session && row.seq >= writer.started.seq)
26
+ return yield* Effect.fail(invalid("selected memory snapshot is outside this turn's history"));
27
+ const data = yield* storedJson(row.data).pipe(Effect.flatMap(Schema.decodeUnknownEffect(MemorySnapshotData)), Effect.mapError((cause) => invalid("selected memory snapshot did not decode", cause))), all = yield* historyLocated, publicationSession = all.findIndex(({ event }) => event.session === row.session), before = (item, index) => item.event.session === row.session ? item.event.seq < row.seq : publicationSession >= 0 && index < publicationSession, anchor = all.findIndex(({ event }) => event.session === data.anchor.session && event.seq === data.anchor.seq), through = all.findIndex(({ event }) => event.session === data.through.session && event.seq === data.through.seq);
28
+ if (anchor < 0 || through < anchor || !before(all[anchor], anchor) || !before(all[through], through) || all[anchor].entry.body._tag !== "TurnStarted")
29
+ return yield* Effect.fail(invalid("selected memory snapshot cursors do not identify a visible kept turn and frontier"));
30
+ return Option.some({ strategy: data.strategy, anchor: all[anchor].entry.id, through: all[through].entry.id, entries: data.entries });
31
+ })),
32
+ save: Effect.fnUntraced(function* (input) {
33
+ const encoded = yield* candidateJson(input), candidate = yield* Schema.decodeUnknownEffect(MemorySnapshotCandidate)(encoded).pipe(Effect.mapError((cause) => invalid("memory snapshot candidate did not decode", cause))), key = canonicalJson(encoded);
34
+ return yield* gate.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
35
+ const previous = yield* Ref.get(terminal);
36
+ if (Option.isSome(previous) && previous.value.key === key)
37
+ return yield* previous.value.exit;
38
+ const exit = yield* Effect.gen(function* () {
39
+ const actual = yield* refresh, anchor = actual.findIndex(({ entry }) => entry.id === candidate.anchor), through = actual.findIndex(({ entry }) => entry.id === candidate.through);
40
+ if (anchor < 0 || through < 0)
41
+ return;
42
+ if (through < anchor || actual[anchor].entry.body._tag !== "TurnStarted")
43
+ return yield* Effect.fail(invalid("memory snapshot candidate has an invalid source frontier"));
44
+ const expected = yield* prepareMemorySnapshot(actual.slice(0, through + 1).map(({ entry }) => entry), candidate.strategy);
45
+ if (Option.isNone(expected) || canonicalJson(yield* candidateJson(expected.value)) !== key)
46
+ return yield* Effect.fail(invalid("memory snapshot candidate differs from its durable source"));
47
+ const data = yield* Schema.encodeEffect(MemorySnapshotData)({
48
+ version: 1,
49
+ strategy: candidate.strategy,
50
+ anchor: { session: actual[anchor].event.session, seq: actual[anchor].event.seq },
51
+ through: { session: actual[through].event.session, seq: actual[through].event.seq },
52
+ entries: candidate.entries
53
+ }).pipe(Effect.flatMap(storedJson), Effect.mapError((cause) => invalid("memory snapshot did not encode", cause)));
54
+ yield* writer.append([frameworkDraft("memory.snapshot", data)]);
55
+ yield* writer.flush;
56
+ }).pipe(Effect.interruptible, Effect.exit);
57
+ yield* Ref.set(terminal, Option.some({ key, exit }));
58
+ return yield* exit;
59
+ })));
60
+ })
61
+ };
62
+ });
@@ -0,0 +1,9 @@
1
+ import { Effect } from "effect";
2
+ import { HarnessError, ModelCallData, ModelCallLegacyUsage } from "@xandreed/core";
3
+ import type { ModelCallRecorder, TurnError, TurnWriter } from "@xandreed/core";
4
+ /** One writer owns durability; observers only see accepted terminal facts. */
5
+ export declare const makeTurnModelCallRecorder: (writer: TurnWriter, publish: (data: ModelCallData) => Effect.Effect<void, TurnError>) => Effect.Effect<{
6
+ recorder: ModelCallRecorder;
7
+ close: Effect.Effect<void, never, never>;
8
+ legacy: (data: ModelCallLegacyUsage) => Effect.Effect<void, HarnessError, never>;
9
+ }, TurnError, never>;
@@ -0,0 +1,93 @@
1
+ import { Context, Deferred, Effect, Exit, Option, Ref, Schema, Semaphore } from "effect";
2
+ import {
3
+ canonicalJson,
4
+ frameworkDraft,
5
+ HarnessError,
6
+ ModelCallData,
7
+ ModelCallLegacyUsage,
8
+ readStored,
9
+ replayableContext,
10
+ storedJson,
11
+ turnModelCallsOf
12
+ } from "@xandreed/core";
13
+ import { CurrentModelCallNotifications, CurrentToolResultReceipt } from "@xandreed/core/internal";
14
+ import { ModelCallRecorderPhase } from "./model-call-recorder.entity.js";
15
+ import { mergeModelCalls, modelCallKey } from "./model-call-recorder.entity.functions.js";
16
+ const invalid = (message, cause) => new HarnessError({ code: "events.encode", message, ...cause === void 0 ? {} : { cause } }), CallText = Schema.fromJsonString(ModelCallData), LegacyText = Schema.fromJsonString(ModelCallLegacyUsage), LegacyListText = Schema.fromJsonString(Schema.Array(ModelCallLegacyUsage)), copyCall = (data) => Schema.encodeEffect(CallText)(data).pipe(Effect.flatMap(Schema.decodeUnknownEffect(CallText))), copyLegacy = (data) => Schema.encodeEffect(LegacyListText)(data).pipe(Effect.flatMap(Schema.decodeUnknownEffect(LegacyListText)));
17
+ export const makeTurnModelCallRecorder = Effect.fnUntraced(function* (writer, publish) {
18
+ const ownRows = (kinds) => writer.snapshot(kinds, writer.started.seq), before = yield* ownRows(["model.called", "step.usage"]), seededCalls = yield* turnModelCallsOf(before, writer.admitted.turn).pipe(Effect.mapError((cause) => invalid("the turn's model-call ledger did not decode", cause))), seededLegacy = yield* readStored("step.usage", before.filter((event) => Option.contains(event.turn, writer.admitted.turn))).pipe(Effect.map((records) => records.map(({ data }) => ({ model: data.model, usage: data.usage, run: data.run, step: data.step, replayed: !0 }))), Effect.mapError((cause) => invalid("the turn's selected usage did not decode", cause))), originalKeys = new Set(seededCalls.map(modelCallKey)), phase = yield* Ref.make({ _tag: "Open", calls: seededCalls.map((data) => ({ data, replayed: !0 })), legacy: seededLegacy }), claims = yield* Ref.make(new Map), observers = yield* Ref.make([]), order = yield* Semaphore.make(1), deliver = (observer, call) => Ref.modify(observer.seen, (seen) => {
19
+ const key = modelCallKey(call.data);
20
+ return seen.has(key) ? [!1, seen] : [!0, new Set([...seen, key])];
21
+ }).pipe(Effect.flatMap((fresh) => fresh ? copyCall(call.data).pipe(Effect.orDie, Effect.flatMap((snapshot) => observer.listener.called(snapshot, { replayed: call.replayed })), Effect.provide(observer.context)) : Effect.void), Effect.interruptible, Effect.ignoreCause({ log: "Warn", message: "a model-call billing observer failed" })), deliverAll = (listeners, calls) => Effect.forEach(listeners, (observer) => Effect.forEach(calls, (call) => deliver(observer, call), { concurrency: 1, discard: !0 }), { concurrency: 1, discard: !0 }), reconcile = ownRows(["model.called"]).pipe(Effect.flatMap((events) => turnModelCallsOf(events, writer.admitted.turn)), Effect.flatMap((calls) => Ref.update(phase, (current) => mergeModelCalls(current, calls.map((data) => ({ data, replayed: originalKeys.has(modelCallKey(data)) }))))), Effect.ignoreCause({ log: "Warn", message: "the model-call billing snapshot could not be refreshed" })), recorder = {
22
+ record: Effect.fn("turn.record_model_call")(function* (data) {
23
+ const encoded = yield* Schema.encodeEffect(ModelCallData)(data).pipe(Effect.flatMap(storedJson), Effect.mapError((cause) => invalid("model.called did not encode", cause))), fact = yield* Schema.decodeUnknownEffect(ModelCallData)(encoded).pipe(Effect.mapError((cause) => invalid("model.called did not decode", cause)));
24
+ if (Option.exists(fact.runId, (runId) => runId !== writer.admitted.runId))
25
+ return yield* Effect.fail(invalid("model.called belongs to another admitted run"));
26
+ const key = modelCallKey(fact), text = canonicalJson(encoded);
27
+ return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
28
+ const done = yield* Deferred.make(), fresh = { encoded: text, done }, claim = yield* Ref.modify(claims, (all) => {
29
+ const prior = all.get(key);
30
+ return prior === void 0 ? [{ _tag: "Own", claim: fresh }, new Map([...all, [key, fresh]])] : [{ _tag: "Join", claim: prior }, all];
31
+ });
32
+ if (claim.claim.encoded !== text)
33
+ return yield* Effect.fail(invalid("model.called repeated a terminal key with different facts"));
34
+ const notifying = yield* CurrentModelCallNotifications;
35
+ if (claim._tag === "Join") {
36
+ if (notifying.has(claim.claim) && !(yield* Deferred.isDone(claim.claim.done)))
37
+ return yield* Effect.fail(new HarnessError({ code: "events.depth", message: "model.called cannot re-enter its own terminal recording notification" }));
38
+ return yield* Deferred.await(claim.claim.done).pipe(Effect.interruptible);
39
+ }
40
+ const operation = Effect.gen(function* () {
41
+ const listeners = yield* order.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
42
+ const current = yield* Ref.get(phase);
43
+ if (ModelCallRecorderPhase.guards.Closed(current))
44
+ return yield* Effect.fail(new HarnessError({ code: "turn.closed", message: "the model-call recorder's owner is closed" }));
45
+ const existing = current.calls.find((call) => modelCallKey(call.data) === key);
46
+ if (existing !== void 0) {
47
+ const known = yield* Schema.encodeEffect(ModelCallData)(existing.data).pipe(Effect.flatMap(storedJson), Effect.mapError((cause) => invalid("model.called did not encode", cause)));
48
+ if (canonicalJson(known) !== text)
49
+ return yield* Effect.fail(invalid("model.called repeated a durable key with different facts"));
50
+ return Option.none();
51
+ }
52
+ yield* writer.append([frameworkDraft("model.called", encoded)]);
53
+ yield* writer.flush;
54
+ yield* Ref.update(phase, (state) => ({ ...state, calls: [...state.calls, { data: fact, replayed: !1 }] }));
55
+ return Option.some(yield* Ref.get(observers));
56
+ })));
57
+ yield* Option.match(listeners, {
58
+ onNone: () => Effect.void,
59
+ onSome: (all) => deliverAll(all, [{ data: fact, replayed: !1 }]).pipe(Effect.andThen(copyCall(fact).pipe(Effect.mapError((cause) => invalid("model.called notification did not encode", cause)), Effect.flatMap(publish))), Effect.provideService(CurrentModelCallNotifications, new Set([...notifying, claim.claim])), Effect.interruptible)
60
+ });
61
+ }), exit = yield* Effect.exit(restore(operation));
62
+ yield* Deferred.done(claim.claim.done, exit);
63
+ return yield* exit;
64
+ }));
65
+ }),
66
+ observe: Effect.fn("turn.observe_model_calls")(function* (listener) {
67
+ const observer = { listener, context: Context.omit(CurrentModelCallNotifications, CurrentToolResultReceipt)(replayableContext(yield* Effect.context())), seen: yield* Ref.make(new Set) }, current = yield* order.withPermits(1)(Effect.gen(function* () {
68
+ yield* reconcile;
69
+ const current = yield* Ref.get(phase);
70
+ if (ModelCallRecorderPhase.guards.Open(current))
71
+ yield* Ref.update(observers, (all) => [...all, observer]);
72
+ return current;
73
+ }));
74
+ yield* deliverAll([observer], current.calls);
75
+ if (ModelCallRecorderPhase.guards.Closed(current))
76
+ yield* copyLegacy(current.legacy).pipe(Effect.orDie, Effect.flatMap(listener.closed), Effect.provide(observer.context), Effect.ignoreCause({ log: "Warn", message: "a model-call billing observer failed to close" }));
77
+ })
78
+ }, close = yield* Effect.cached(Effect.gen(function* () {
79
+ const ending = yield* order.withPermits(1)(Effect.gen(function* () {
80
+ yield* reconcile;
81
+ const current = yield* Ref.get(phase), closed = { ...current, _tag: "Closed" };
82
+ yield* Ref.set(phase, closed);
83
+ return { current, listeners: yield* Ref.get(observers) };
84
+ }));
85
+ yield* deliverAll(ending.listeners, ending.current.calls);
86
+ yield* Effect.forEach(ending.listeners, (observer) => copyLegacy(ending.current.legacy).pipe(Effect.orDie, Effect.flatMap(observer.listener.closed), Effect.provide(observer.context), Effect.ignoreCause({ log: "Warn", message: "a model-call billing observer failed to close" })), { concurrency: 1, discard: !0 });
87
+ }));
88
+ return {
89
+ recorder,
90
+ close,
91
+ legacy: (data) => Schema.encodeEffect(LegacyText)(data).pipe(Effect.flatMap(Schema.decodeUnknownEffect(LegacyText)), Effect.mapError((cause) => invalid("selected usage did not encode", cause)), Effect.flatMap((snapshot) => Ref.update(phase, (current) => ModelCallRecorderPhase.guards.Open(current) ? { ...current, legacy: [...current.legacy, snapshot] } : current)))
92
+ };
93
+ });
@@ -0,0 +1,145 @@
1
+ import { Schema } from "effect";
2
+ export declare const ObservedModelCall: Schema.Struct<{
3
+ readonly data: Schema.Struct<{
4
+ readonly callId: Schema.brand<Schema.Trimmed, "ModelCallId">;
5
+ readonly attempt: Schema.Int;
6
+ readonly purpose: Schema.Literals<readonly ["Loop", "Helper"]>;
7
+ readonly method: Schema.Literals<readonly ["Text", "Object", "Stream"]>;
8
+ readonly model: Schema.OptionFromNullOr<typeof import("@xandreed/core").ModelSelection>;
9
+ readonly runId: Schema.OptionFromNullOr<Schema.brand<Schema.String, "RunId">>;
10
+ readonly run: Schema.OptionFromNullOr<Schema.brand<Schema.Int, "RunOrdinal">>;
11
+ readonly step: Schema.OptionFromNullOr<Schema.Int>;
12
+ readonly durationMs: Schema.Finite;
13
+ readonly usage: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unknown", {}>, Schema.TaggedStruct<"Known", {
14
+ readonly usage: Schema.Struct<{
15
+ readonly inputTokens: Schema.Int;
16
+ readonly outputTokens: Schema.Int;
17
+ readonly totalTokens: Schema.Int;
18
+ readonly cacheReadTokens: Schema.Int;
19
+ readonly cacheWriteTokens: Schema.Int;
20
+ readonly reasoningTokens: Schema.Int;
21
+ }>;
22
+ }>, Schema.TaggedStruct<"Partial", {
23
+ readonly usage: Schema.Struct<{
24
+ readonly inputTokens: Schema.Int;
25
+ readonly outputTokens: Schema.Int;
26
+ readonly totalTokens: Schema.Int;
27
+ readonly cacheReadTokens: Schema.Int;
28
+ readonly cacheWriteTokens: Schema.Int;
29
+ readonly reasoningTokens: Schema.Int;
30
+ }>;
31
+ readonly reported: Schema.$Array<Schema.Literals<readonly ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]>>;
32
+ }>]>;
33
+ readonly outcome: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Completed", {}>, Schema.TaggedStruct<"Failed", {
34
+ readonly failure: Schema.OptionFromNullOr<Schema.Literals<readonly ["ContextOverflow", "Authentication", "InvalidRequest", "RateLimit", "Quota", "Transient", "InvalidResponse", "Timeout", "Unknown"]>>;
35
+ }>, Schema.TaggedStruct<"Interrupted", {}>]>;
36
+ }>;
37
+ readonly replayed: Schema.Boolean;
38
+ }>;
39
+ export type ObservedModelCall = typeof ObservedModelCall.Type;
40
+ export declare const ModelCallRecorderPhase: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Open", {
41
+ calls: Schema.$Array<Schema.Struct<{
42
+ readonly data: Schema.Struct<{
43
+ readonly callId: Schema.brand<Schema.Trimmed, "ModelCallId">;
44
+ readonly attempt: Schema.Int;
45
+ readonly purpose: Schema.Literals<readonly ["Loop", "Helper"]>;
46
+ readonly method: Schema.Literals<readonly ["Text", "Object", "Stream"]>;
47
+ readonly model: Schema.OptionFromNullOr<typeof import("@xandreed/core").ModelSelection>;
48
+ readonly runId: Schema.OptionFromNullOr<Schema.brand<Schema.String, "RunId">>;
49
+ readonly run: Schema.OptionFromNullOr<Schema.brand<Schema.Int, "RunOrdinal">>;
50
+ readonly step: Schema.OptionFromNullOr<Schema.Int>;
51
+ readonly durationMs: Schema.Finite;
52
+ readonly usage: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unknown", {}>, Schema.TaggedStruct<"Known", {
53
+ readonly usage: Schema.Struct<{
54
+ readonly inputTokens: Schema.Int;
55
+ readonly outputTokens: Schema.Int;
56
+ readonly totalTokens: Schema.Int;
57
+ readonly cacheReadTokens: Schema.Int;
58
+ readonly cacheWriteTokens: Schema.Int;
59
+ readonly reasoningTokens: Schema.Int;
60
+ }>;
61
+ }>, Schema.TaggedStruct<"Partial", {
62
+ readonly usage: Schema.Struct<{
63
+ readonly inputTokens: Schema.Int;
64
+ readonly outputTokens: Schema.Int;
65
+ readonly totalTokens: Schema.Int;
66
+ readonly cacheReadTokens: Schema.Int;
67
+ readonly cacheWriteTokens: Schema.Int;
68
+ readonly reasoningTokens: Schema.Int;
69
+ }>;
70
+ readonly reported: Schema.$Array<Schema.Literals<readonly ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]>>;
71
+ }>]>;
72
+ readonly outcome: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Completed", {}>, Schema.TaggedStruct<"Failed", {
73
+ readonly failure: Schema.OptionFromNullOr<Schema.Literals<readonly ["ContextOverflow", "Authentication", "InvalidRequest", "RateLimit", "Quota", "Transient", "InvalidResponse", "Timeout", "Unknown"]>>;
74
+ }>, Schema.TaggedStruct<"Interrupted", {}>]>;
75
+ }>;
76
+ readonly replayed: Schema.Boolean;
77
+ }>>;
78
+ legacy: Schema.$Array<Schema.Struct<{
79
+ readonly model: Schema.OptionFromNullOr<Schema.String>;
80
+ readonly usage: Schema.Struct<{
81
+ readonly inputTokens: Schema.Number;
82
+ readonly outputTokens: Schema.Number;
83
+ readonly totalTokens: Schema.Number;
84
+ readonly cacheReadTokens: Schema.Number;
85
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
86
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
87
+ }>;
88
+ readonly run: Schema.brand<Schema.Int, "RunOrdinal">;
89
+ readonly step: Schema.Int;
90
+ readonly replayed: Schema.Boolean;
91
+ }>>;
92
+ }>, Schema.TaggedStruct<"Closed", {
93
+ calls: Schema.$Array<Schema.Struct<{
94
+ readonly data: Schema.Struct<{
95
+ readonly callId: Schema.brand<Schema.Trimmed, "ModelCallId">;
96
+ readonly attempt: Schema.Int;
97
+ readonly purpose: Schema.Literals<readonly ["Loop", "Helper"]>;
98
+ readonly method: Schema.Literals<readonly ["Text", "Object", "Stream"]>;
99
+ readonly model: Schema.OptionFromNullOr<typeof import("@xandreed/core").ModelSelection>;
100
+ readonly runId: Schema.OptionFromNullOr<Schema.brand<Schema.String, "RunId">>;
101
+ readonly run: Schema.OptionFromNullOr<Schema.brand<Schema.Int, "RunOrdinal">>;
102
+ readonly step: Schema.OptionFromNullOr<Schema.Int>;
103
+ readonly durationMs: Schema.Finite;
104
+ readonly usage: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unknown", {}>, Schema.TaggedStruct<"Known", {
105
+ readonly usage: Schema.Struct<{
106
+ readonly inputTokens: Schema.Int;
107
+ readonly outputTokens: Schema.Int;
108
+ readonly totalTokens: Schema.Int;
109
+ readonly cacheReadTokens: Schema.Int;
110
+ readonly cacheWriteTokens: Schema.Int;
111
+ readonly reasoningTokens: Schema.Int;
112
+ }>;
113
+ }>, Schema.TaggedStruct<"Partial", {
114
+ readonly usage: Schema.Struct<{
115
+ readonly inputTokens: Schema.Int;
116
+ readonly outputTokens: Schema.Int;
117
+ readonly totalTokens: Schema.Int;
118
+ readonly cacheReadTokens: Schema.Int;
119
+ readonly cacheWriteTokens: Schema.Int;
120
+ readonly reasoningTokens: Schema.Int;
121
+ }>;
122
+ readonly reported: Schema.$Array<Schema.Literals<readonly ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]>>;
123
+ }>]>;
124
+ readonly outcome: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Completed", {}>, Schema.TaggedStruct<"Failed", {
125
+ readonly failure: Schema.OptionFromNullOr<Schema.Literals<readonly ["ContextOverflow", "Authentication", "InvalidRequest", "RateLimit", "Quota", "Transient", "InvalidResponse", "Timeout", "Unknown"]>>;
126
+ }>, Schema.TaggedStruct<"Interrupted", {}>]>;
127
+ }>;
128
+ readonly replayed: Schema.Boolean;
129
+ }>>;
130
+ legacy: Schema.$Array<Schema.Struct<{
131
+ readonly model: Schema.OptionFromNullOr<Schema.String>;
132
+ readonly usage: Schema.Struct<{
133
+ readonly inputTokens: Schema.Number;
134
+ readonly outputTokens: Schema.Number;
135
+ readonly totalTokens: Schema.Number;
136
+ readonly cacheReadTokens: Schema.Number;
137
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
138
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
139
+ }>;
140
+ readonly run: Schema.brand<Schema.Int, "RunOrdinal">;
141
+ readonly step: Schema.Int;
142
+ readonly replayed: Schema.Boolean;
143
+ }>>;
144
+ }>]>;
145
+ export type ModelCallRecorderPhase = typeof ModelCallRecorderPhase.Type;
@@ -0,0 +1,5 @@
1
+ import type { ModelCallData } from "@xandreed/core";
2
+ import type { ModelCallRecorderPhase, ObservedModelCall } from "./model-call-recorder.entity.js";
3
+ export declare const modelCallKey: (data: ModelCallData) => string;
4
+ /** Fresh durable order wins; retain facts whose accepted ACK is newer than the snapshot. */
5
+ export declare const mergeModelCalls: (phase: ModelCallRecorderPhase, calls: ReadonlyArray<ObservedModelCall>) => ModelCallRecorderPhase;
@@ -0,0 +1,4 @@
1
+ export const modelCallKey = (data) => `${data.callId}:${data.attempt}`, mergeModelCalls = (phase, calls) => {
2
+ const keys = new Set(calls.map((call) => modelCallKey(call.data)));
3
+ return { ...phase, calls: [...calls, ...phase.calls.filter((call) => !keys.has(modelCallKey(call.data)))] };
4
+ };
@@ -0,0 +1,8 @@
1
+ import { Schema } from "effect";
2
+ import { ModelCallData, ModelCallLegacyUsage } from "@xandreed/core";
3
+ export const ObservedModelCall = Schema.Struct({ data: ModelCallData, replayed: Schema.Boolean });
4
+ const facts = { calls: Schema.Array(ObservedModelCall), legacy: Schema.Array(ModelCallLegacyUsage) };
5
+ export const ModelCallRecorderPhase = Schema.Union([
6
+ Schema.TaggedStruct("Open", facts),
7
+ Schema.TaggedStruct("Closed", facts)
8
+ ]).pipe(Schema.toTaggedUnion("_tag"));
@@ -0,0 +1,61 @@
1
+ import { Context, Effect } from "effect";
2
+ import type { Scope } from "effect";
3
+ import { ToolRegistry } from "@xandreed/core";
4
+ import type { Capability } from "@xandreed/core";
5
+ import type { ConformanceCheck } from "@xandreed/core/conformance";
6
+ type Registry = Context.Service.Shape<typeof ToolRegistry>;
7
+ /**
8
+ * What the kit asks of the registry it builds: the most calls a turn may make.
9
+ *
10
+ * @public
11
+ */
12
+ export interface ToolRegistryLimits {
13
+ readonly maxCallsPerRun: number;
14
+ readonly readConcurrency?: number;
15
+ readonly toolPolicy?: Readonly<Record<string, {
16
+ readonly concurrency?: number;
17
+ readonly maxCallsPerRun?: number;
18
+ readonly breakAfter?: number;
19
+ readonly breakOn?: "defects" | "failures";
20
+ }>>;
21
+ }
22
+ /**
23
+ * The ToolRegistry contract, as checks: the catalog and the run's tools
24
+ * hold every tool and skill the capabilities define; always-on skills are
25
+ * active once the matcher's choice is applied, other skills only once
26
+ * activated, an unknown skill is refused (`skills.unknown`), a skill whose
27
+ * permission is not granted is not activated, and the active set only grows,
28
+ * publishing `skills.activated` for what is new; `load_skill` refuses an
29
+ * unknown skill and an ungranted one as the call's result, under a
30
+ * PascalCase name and never the host's code; `load_skill` calls running
31
+ * at once (tool concurrency above 1) leave every loaded tool active and
32
+ * listed, each published and recorded once; a handled call publishes
33
+ * `tool.started`, its handler's `tool.progress` and `tool.completed` under
34
+ * one invocation id (`<runId>:call:<n>`), and the handler sees that call
35
+ * as `CurrentToolInvocation` and the services where the tools were opened,
36
+ * the ones it declares (`RegisteredTool.requires`) included. The provider's
37
+ * pairing key reaches the invocation, every lifecycle event and its journal
38
+ * record as `toolCallId` (absent on direct calls without one). A run lacking
39
+ * a service a tool declares fails to open its tools with
40
+ * `requires.missing`, naming the tool and the service; and a call refused
41
+ * before its handler runs (`inactive`, `forbidden`, `denied`, `call-limit`,
42
+ * `tool-call-limit`) takes its invocation id first, publishes one
43
+ * `tool.refused` with its input and no `tool.started`, never runs the
44
+ * handler, and fails with the refusal's message as its result, under a
45
+ * PascalCase name (`Failure.error`).
46
+ * The registry publishes these events on the run's bus (`RunContext.events`)
47
+ * only, and never writes the journal itself: the turn journals them (the sdk's
48
+ * `TurnLive` keeps `tool.progress` out and stores a `tool.refused` without
49
+ * its input). The kit reads the journal to catch a registry that writes it
50
+ * directly; what the turn does with the events is `TurnLive`'s, not the
51
+ * registry's.
52
+ *
53
+ * `make` builds a registry over the kit's capabilities for each check, with
54
+ * at most `maxCallsPerRun` calls per turn and no permissions granted unless
55
+ * the turn's `PermissionGrants` say so. The kit runs each turn over the sdk's
56
+ * `TurnLive` with a writer that keeps the turn's events in memory, opens
57
+ * the tools with a `SessionEnvironment` (which one of its tools declares)
58
+ * and calls them with another, as a step would.
59
+ */
60
+ export declare const toolRegistryConformance: (make: (capabilities: ReadonlyArray<Capability>, limits: ToolRegistryLimits) => Effect.Effect<Registry, unknown, Scope.Scope>) => ReadonlyArray<ConformanceCheck>;
61
+ export {};