@xandreed/sdk 0.14.0 → 0.15.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/README.md +1 -1
- package/dist/agent.adapter.d.ts +66 -34
- package/dist/agent.adapter.js +48 -37
- package/dist/conformance/index.d.ts +2 -0
- package/dist/conformance/index.js +1 -0
- package/dist/inbox-runner.adapter.d.ts +18 -0
- package/dist/inbox-runner.adapter.js +8 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.js +59 -2
- package/dist/task-executor.adapter.d.ts +32 -0
- package/dist/task-executor.adapter.js +10 -0
- package/dist/task-inbox-runner.adapter.d.ts +11 -0
- package/dist/task-inbox-runner.adapter.js +22 -0
- package/dist/turn/memory-checkpoint.adapter.d.ts +17 -0
- package/dist/turn/memory-checkpoint.adapter.js +62 -0
- package/dist/turn/model-call-recorder.adapter.d.ts +9 -0
- package/dist/turn/model-call-recorder.adapter.js +93 -0
- package/dist/turn/model-call-recorder.entity.d.ts +145 -0
- package/dist/turn/model-call-recorder.entity.functions.d.ts +5 -0
- package/dist/turn/model-call-recorder.entity.functions.js +4 -0
- package/dist/turn/model-call-recorder.entity.js +8 -0
- package/dist/turn/tool-registry.conformance.d.ts +61 -0
- package/dist/turn/tool-registry.conformance.js +547 -0
- package/dist/turn/turn-after-reply.entity.functions.d.ts +9 -0
- package/dist/turn/turn-after-reply.entity.functions.js +4 -0
- package/dist/turn/turn-lifecycle.usecase.functions.d.ts +135 -0
- package/dist/turn/turn-lifecycle.usecase.functions.js +131 -0
- package/dist/turn/turn-observation.entity.d.ts +28 -0
- package/dist/turn/turn-observation.entity.functions.d.ts +15 -0
- package/dist/turn/turn-observation.entity.functions.js +17 -0
- package/dist/turn/turn-observation.entity.js +12 -0
- package/dist/turn/turn-overflow.entity.d.ts +7 -0
- package/dist/turn/turn-overflow.entity.functions.d.ts +11 -0
- package/dist/turn/turn-overflow.entity.functions.js +12 -0
- package/dist/turn/turn-overflow.entity.js +5 -0
- package/dist/turn/turn-phase.entity.d.ts +462 -0
- package/dist/turn/turn-phase.entity.functions.d.ts +44 -0
- package/dist/turn/turn-phase.entity.functions.js +49 -0
- package/dist/turn/turn-phase.entity.js +13 -0
- package/dist/turn/turn-run.usecase.functions.d.ts +72 -0
- package/dist/turn/turn-run.usecase.functions.js +329 -0
- package/dist/turn/turn.adapter.d.ts +26 -0
- package/dist/turn/turn.adapter.js +225 -0
- package/package.json +10 -4
- package/dist/activity.test.d.ts +0 -1
- package/dist/agent.adapter.test.d.ts +0 -1
- package/dist/agent.backends.test.d.ts +0 -1
- package/dist/agent.golden.test.d.ts +0 -231
- package/dist/agent.trace.test.d.ts +0 -1
- package/dist/events-guide.test.d.ts +0 -207
- package/dist/journal-compat.test.d.ts +0 -1
- package/dist/memory.conformance.test.d.ts +0 -1
- package/dist/models-plugin.test.d.ts +0 -1
- package/dist/plugin-stack.test.d.ts +0 -1
- package/dist/resume.journey.test.d.ts +0 -1
- package/dist/testing.port.d.ts +0 -16
- package/dist/testing.port.js +0 -7
- 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 {};
|