@xandreed/sdk 0.13.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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/dist/agent.adapter.d.ts +111 -11
  3. package/dist/agent.adapter.js +80 -34
  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 +34 -1
  9. package/dist/index.js +58 -1
  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/agent.adapter.test.d.ts +0 -1
  46. package/dist/agent.backends.test.d.ts +0 -1
  47. package/dist/agent.golden.test.d.ts +0 -190
  48. package/dist/memory.conformance.test.d.ts +0 -1
  49. package/dist/plugin-stack.test.d.ts +0 -1
  50. package/dist/testing.port.d.ts +0 -16
  51. package/dist/testing.port.js +0 -7
  52. package/dist/turn-composition.test.d.ts +0 -1
@@ -0,0 +1,135 @@
1
+ import { Effect } from "effect";
2
+ import { RunContext, TurnEvents, TurnMemory, TurnTasks, TurnToolbox } from "@xandreed/core";
3
+ import type { AdmittedTurn, ConversationId, RunId, RunTools, StepLoop, Turn, TurnError, TurnOutcome, TurnResult, TurnRunOptions, TurnTools, TurnWriter } from "@xandreed/core";
4
+ import type { TurnRunServices } from "./turn-run.usecase.functions.js";
5
+ /**
6
+ * Open the turn's tools (once). Their handlers run with the services of where this runs: open them inside the host's layer.
7
+ *
8
+ * @public
9
+ */
10
+ export declare const openTurnTools: Effect.Effect<RunTools, TurnError, TurnToolbox>;
11
+ /**
12
+ * The host's view of the open tools; skills the host activates are recorded
13
+ * as its own.
14
+ *
15
+ * @public
16
+ */
17
+ export declare const turnTools: Effect.Effect<TurnTools, TurnError, TurnToolbox>;
18
+ /**
19
+ * Await the tasks, then drain the background reactions, again until a round starts nothing new.
20
+ *
21
+ * @public
22
+ */
23
+ export declare const settleTurn: Effect.Effect<void, TurnError, TurnEvents | TurnTasks>;
24
+ /**
25
+ * Record the reply (`persistReply`), drain the reactions to it, and flush
26
+ * the journal. The reply is recorded and the journal flushed even when the
27
+ * fiber is interrupted meanwhile; only the drain can be cut. The reply is
28
+ * stored with the turn's end; when the drain or the flush fails, or the
29
+ * drain is cut, it is downgraded to failed first.
30
+ *
31
+ * @public
32
+ */
33
+ export declare const finishTurn: (outcome: TurnOutcome) => Effect.Effect<void, TurnError, TurnMemory | TurnEvents | RunContext>;
34
+ /**
35
+ * Run a turn's body to its end: once it succeeds, the turn settles (tasks
36
+ * and background reactions) and finishes with its outcome, returned with
37
+ * the turn's usage (`RunContext.usage`, the same total `turn.ended`
38
+ * carries). Once the body has returned, its reply is recorded and the
39
+ * journal flushed even when the turn is interrupted then; an interruption
40
+ * cuts only the drain of the reactions to the reply. The reply is stored
41
+ * with the turn's end (`TurnWriter.closing`): when that finishing fails or
42
+ * is cut, the turn fails with its cause and the reply is downgraded to
43
+ * failed, so the end never stores a completed reply. When the body fails or
44
+ * is interrupted, the turn finishes as failed and keeps the body's cause:
45
+ * the failed reply is recorded, the reactions to it are waited for at most
46
+ * ten seconds (not at all once interrupted), a failure of that finishing is
47
+ * logged, and the usage its provider calls spent so far goes on the current
48
+ * span as `efferent.usage.*_tokens` (the turn's `agent.turn` when
49
+ * `guardTurn` runs under `traceTurn` with no span between, as in
50
+ * `Agent.turn`; `traceTurn` records it itself when the turn returns). A
51
+ * `TurnError` carried as a defect (a subscriber's or the writer's failure
52
+ * behind a `never` error channel) fails the turn, typed.
53
+ *
54
+ * @public
55
+ */
56
+ export declare const guardTurn: <E, R>(body: Effect.Effect<TurnOutcome, E, R>) => Effect.Effect<TurnResult, E | TurnError, R | TurnMemory | TurnEvents | TurnTasks | RunContext>;
57
+ /**
58
+ * The `Turn` a host's code works with, over the turn's services (the one
59
+ * `Agent.turn` hands to `use`): its `run` is `runTurnLoop` with `options`,
60
+ * its spans under the span current where `run` is called. The message must
61
+ * be persisted and the tools open.
62
+ *
63
+ * @public
64
+ */
65
+ export declare const turnOf: (options?: TurnRunOptions | undefined) => Effect.Effect<Turn, TurnError, RunContext | StepLoop | TurnRunServices>;
66
+ /**
67
+ * What a turn's span (`traceTurn`) shows besides the turn's ids.
68
+ *
69
+ * @public
70
+ */
71
+ export interface TurnTraceOptions {
72
+ /**
73
+ * The keys of the turn's metadata (`AdmittedTurn.meta`) whose values the
74
+ * span carries, as `efferent.meta.<key>`: none by default, since metadata
75
+ * may hold what a trace backend should not (a user, a tenant). A string,
76
+ * number or boolean is kept as it is, anything else as its JSON; a key the
77
+ * turn's metadata lacks is left out.
78
+ */
79
+ readonly spanMeta?: ReadonlyArray<string>;
80
+ }
81
+ /**
82
+ * Run a turn under its span, `agent.turn`, the root of everything the turn
83
+ * does (under the caller's span, if any). It starts once the turn is
84
+ * admitted (`writer`), with `efferent.session.id`, `efferent.run.id`,
85
+ * `efferent.turn`, `gen_ai.conversation.id` (the session), `efferent.origin`,
86
+ * `efferent.resumes` (on a resume) and `options.spanMeta`'s
87
+ * `efferent.meta.<key>`; every span under it
88
+ * carries the first three too. It ends with `efferent.outcome` (the
89
+ * `TurnEnding` reason of the turn's exit: completed, partial, failed or
90
+ * interrupted; a turn cancelled or reaped while it ran shows as failed with
91
+ * `turn.closed`, its `TurnClosed`'s code), `efferent.failure.code` when the turn failed or was
92
+ * interrupted, and the turn's usage
93
+ * (`efferent.usage.{input,output,cache_read,cache_write,reasoning,total}_tokens`):
94
+ * when it returned, from its result; when it failed, what `guardTurn`
95
+ * records, so `guardTurn` runs under this span with no span between.
96
+ * `Agent.turn` encloses everything after `Sessions.begin`
97
+ * in it: the turn-dependent plugins, the host's layer, the tools opened, `use`,
98
+ * the reply and the end; a host composing its own turn wraps the same. The
99
+ * begin itself has its own span beside this one (`traceTurnBegin`).
100
+ *
101
+ * @public
102
+ */
103
+ export declare const traceTurn: (writer: TurnWriter, options?: TurnTraceOptions) => <E, R>(effect: Effect.Effect<TurnResult, E, R>) => Effect.Effect<TurnResult, E, R>;
104
+ /**
105
+ * A turn about to begin, as its begin span (`traceTurnBegin`) names it.
106
+ *
107
+ * @public
108
+ */
109
+ export interface TurnBeginTrace {
110
+ readonly session: ConversationId;
111
+ readonly runId: RunId;
112
+ /** Who begins it: a user (a `User` begin), the inbox (an `Inbox` begin) or a background task (a `Task` begin). */
113
+ readonly origin: AdmittedTurn["origin"];
114
+ }
115
+ /**
116
+ * Run the work that begins a turn under its span, `agent.turn.begin`, a
117
+ * sibling of the turn's `agent.turn` under the caller's span (if any). It
118
+ * starts with `efferent.session.id`, `efferent.run.id` and `efferent.origin`,
119
+ * and ends with `efferent.turn` once the turn begins, or with
120
+ * `efferent.failure.code` when it cannot (the refusal's code, as
121
+ * `harnessCodeOf` gives it: `session.busy`, `turn.duplicate`…; else
122
+ * `turn.failed`, `turn.defect` or `turn.interrupted`). The work's logs carry
123
+ * the session and run ids as annotations. The work returns what names the
124
+ * admitted turn: its `TurnWriter` (`Sessions.begin`), or a value holding the
125
+ * admitted turn under `admitted`. `Agent.turn` encloses the session-scoped
126
+ * plugins' activation and `Sessions.begin` in it when it begins the turn
127
+ * itself; a turn the host began (its writer passed in) has no begin span. A
128
+ * host composing its own turn wraps its begin the same way, then the rest
129
+ * in `traceTurn`.
130
+ *
131
+ * @public
132
+ */
133
+ export declare const traceTurnBegin: (begin: TurnBeginTrace) => <A extends {
134
+ readonly admitted: AdmittedTurn;
135
+ }, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
@@ -0,0 +1,131 @@
1
+ import { Cause, Context, Effect, Exit, Option } from "effect";
2
+ import { CurrentToolResultReceipt } from "@xandreed/core/internal";
3
+ import {
4
+ harnessDefectsAsFailures,
5
+ replayableContext,
6
+ RunContext,
7
+ sessionAttributes,
8
+ turnAttributes,
9
+ turnEndingOf,
10
+ TurnEvents,
11
+ TurnMemory,
12
+ TurnTasks,
13
+ TurnToolbox
14
+ } from "@xandreed/core";
15
+ import { runTurnLoop } from "./turn-run.usecase.functions.js";
16
+ const failed = { outcome: "failed", reply: Option.none() };
17
+ export const openTurnTools = TurnToolbox.pipe(Effect.flatMap((toolbox) => toolbox.open)), turnTools = TurnToolbox.pipe(Effect.flatMap((toolbox) => toolbox.tools), Effect.map((tools) => ({
18
+ match: tools.match,
19
+ apply: tools.apply,
20
+ select: tools.select,
21
+ activate: (skills) => tools.activate(skills, "host"),
22
+ active: tools.active,
23
+ skills: tools.skills
24
+ }))), settleTurn = Effect.gen(function* () {
25
+ const events = yield* TurnEvents, tasks = yield* TurnTasks, activity = Effect.zipWith(tasks.activity, events.activity, (forked, delivered) => forked + delivered), settle = Effect.gen(function* () {
26
+ const before = yield* activity;
27
+ yield* tasks.await([]);
28
+ yield* events.drain;
29
+ if ((yield* activity) !== before)
30
+ yield* Effect.suspend(() => settle);
31
+ });
32
+ yield* settle;
33
+ });
34
+ const logFailure = (message) => (effect) => effect.pipe(Effect.exit, Effect.flatMap(Exit.match({
35
+ onSuccess: () => Effect.void,
36
+ onFailure: (cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logWarning(message, cause)
37
+ }))), finishing = (restore, outcome) => Effect.gen(function* () {
38
+ const memory = yield* TurnMemory, finished = yield* Effect.exit(Effect.gen(function* () {
39
+ yield* memory.persistReply(outcome);
40
+ const drained = yield* Effect.exit(restore((yield* TurnEvents).drain));
41
+ yield* (yield* RunContext).flush;
42
+ return yield* drained;
43
+ }));
44
+ if (Exit.isFailure(finished)) {
45
+ yield* memory.persistReply(failed).pipe(logFailure("the turn's reply was not downgraded to failed"));
46
+ return yield* Effect.failCause(finished.cause);
47
+ }
48
+ if (outcome.outcome !== "completed" && outcome.outcome !== "partial")
49
+ return;
50
+ const after = yield* Effect.exit(restore(memory.afterReply));
51
+ if (Exit.isFailure(after) && !Cause.hasInterruptsOnly(after.cause))
52
+ return yield* Effect.failCause(after.cause);
53
+ });
54
+ export const finishTurn = (outcome) => Effect.uninterruptibleMask((restore) => finishing(restore, outcome));
55
+ const failedReactionsWait = "10 seconds", finishFailed = Effect.gen(function* () {
56
+ const run = yield* RunContext;
57
+ yield* (yield* TurnMemory).persistReply(failed).pipe(logFailure("the failed turn's reply was not recorded"));
58
+ const settled = yield* (yield* TurnEvents).drain.pipe(Effect.timeoutOption(failedReactionsWait), Effect.interruptible, Effect.exit);
59
+ yield* Exit.match(settled, {
60
+ onSuccess: Option.match({
61
+ onSome: () => Effect.void,
62
+ onNone: () => Effect.logWarning(`the reactions to the failed turn's reply did not settle within ${failedReactionsWait}; the turn ends without them`)
63
+ }),
64
+ onFailure: (cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logWarning("the reactions to the failed turn's reply failed", cause)
65
+ });
66
+ yield* run.flush.pipe(logFailure("the failed turn's journal was not flushed"));
67
+ yield* Effect.annotateCurrentSpan(turnUsageAttributes(yield* run.usage));
68
+ });
69
+ export const guardTurn = (body) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
70
+ const exit = yield* Effect.exit(restore(harnessDefectsAsFailures(body.pipe(Effect.tap(() => settleTurn)))));
71
+ if (Exit.isFailure(exit)) {
72
+ yield* finishFailed;
73
+ return yield* Effect.failCause(exit.cause);
74
+ }
75
+ const outcome = exit.value;
76
+ yield* finishing(restore, outcome);
77
+ const usage = yield* (yield* RunContext).usage;
78
+ return { outcome: outcome.outcome, reply: outcome.reply, usage };
79
+ })), turnOf = Effect.fnUntraced(function* (options = {}) {
80
+ const services = Context.omit(CurrentToolResultReceipt)(replayableContext(yield* Effect.context())), run = yield* RunContext, memory = yield* TurnMemory;
81
+ return {
82
+ conversation: run.conversation,
83
+ runId: run.runId,
84
+ turn: yield* memory.number,
85
+ userMessage: run.userMessage,
86
+ admitted: run.admitted,
87
+ memory: run.memory,
88
+ events: run.events,
89
+ tasks: run.tasks,
90
+ tools: yield* turnTools,
91
+ context: memory.context,
92
+ reply: (text) => Effect.succeed({ outcome: "completed", reply: Option.some(text) }),
93
+ run: (policy) => runTurnLoop(policy, options).pipe(Effect.provide(services)),
94
+ usage: run.usage,
95
+ flush: run.flush,
96
+ write: run.write
97
+ };
98
+ });
99
+ const metaAttribute = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value), turnSpanAttributes = (admitted, spanMeta) => ({
100
+ ...turnAttributes(admitted),
101
+ "gen_ai.conversation.id": admitted.session.id,
102
+ "efferent.origin": admitted.origin,
103
+ ...admitted.resumes === void 0 ? {} : { "efferent.resumes": admitted.resumes },
104
+ ...Object.fromEntries(spanMeta.filter((key) => Object.hasOwn(admitted.meta, key)).map((key) => [`efferent.meta.${key}`, metaAttribute(admitted.meta[key])]))
105
+ }), turnUsageAttributes = (usage) => ({
106
+ "efferent.usage.input_tokens": usage.inputTokens,
107
+ "efferent.usage.output_tokens": usage.outputTokens,
108
+ "efferent.usage.cache_read_tokens": usage.cacheReadTokens,
109
+ "efferent.usage.cache_write_tokens": usage.cacheWriteTokens,
110
+ "efferent.usage.reasoning_tokens": usage.reasoningTokens,
111
+ "efferent.usage.total_tokens": usage.totalTokens
112
+ }), turnEndAttributes = (exit) => {
113
+ const ending = turnEndingOf(exit);
114
+ return {
115
+ "efferent.outcome": ending.reason,
116
+ ...Option.match(ending.failure, { onNone: () => ({}), onSome: (failure) => ({ "efferent.failure.code": failure.code }) }),
117
+ ...Exit.match(exit, { onSuccess: (result) => turnUsageAttributes(result.usage), onFailure: () => ({}) })
118
+ };
119
+ };
120
+ export const traceTurn = (writer, options = {}) => (effect) => effect.pipe(Effect.onExit((exit) => Effect.annotateCurrentSpan(turnEndAttributes(exit))), Effect.annotateSpans(turnAttributes(writer.admitted)), Effect.withSpan("agent.turn", { attributes: turnSpanAttributes(writer.admitted, options.spanMeta ?? []) }));
121
+ const beginAttributes = (begin) => ({
122
+ ...sessionAttributes(begin.session),
123
+ "efferent.run.id": begin.runId
124
+ }), beginEndAttributes = (exit) => Exit.match(exit, {
125
+ onSuccess: (begun) => ({ "efferent.turn": begun.admitted.turn }),
126
+ onFailure: (cause) => Option.match(turnEndingOf(Exit.failCause(cause)).failure, {
127
+ onNone: () => ({}),
128
+ onSome: (failure) => ({ "efferent.failure.code": failure.code })
129
+ })
130
+ });
131
+ export const traceTurnBegin = (begin) => (effect) => effect.pipe(Effect.onExit((exit) => Effect.annotateCurrentSpan(beginEndAttributes(exit))), Effect.annotateLogs(beginAttributes(begin)), Effect.withSpan("agent.turn.begin", { attributes: { ...beginAttributes(begin), "efferent.origin": begin.origin } }));
@@ -0,0 +1,28 @@
1
+ import { Schema } from "effect";
2
+ /** The last accepted response of one prepared run, independent of billing. */
3
+ export declare const TurnObservation: Schema.Struct<{
4
+ readonly observed: Schema.OptionFromNullOr<Schema.Struct<{
5
+ readonly model: typeof import("@xandreed/core").ModelSelection;
6
+ readonly inputTokens: Schema.Int;
7
+ readonly estimatedTokens: Schema.Int;
8
+ readonly reservedTokens: Schema.Int;
9
+ }>>;
10
+ readonly usage: Schema.OptionFromNullOr<Schema.Struct<{
11
+ readonly inputTokens: Schema.Number;
12
+ readonly outputTokens: Schema.Number;
13
+ readonly totalTokens: Schema.Number;
14
+ readonly cacheReadTokens: Schema.Number;
15
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
16
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
17
+ }>>;
18
+ readonly baseReservedTokens: Schema.OptionFromNullOr<Schema.Int>;
19
+ }>;
20
+ export type TurnObservation = typeof TurnObservation.Type;
21
+ /** What a stored `context.built` froze of its request: the step it planned, its message estimate and reserve. */
22
+ export declare const StoredRequestMeasure: Schema.Struct<{
23
+ readonly step: Schema.Int;
24
+ readonly run: Schema.withDecodingDefaultKey<Schema.brand<Schema.Int, "RunOrdinal">, never>;
25
+ readonly estimatedTokens: Schema.Int;
26
+ readonly reservedTokens: Schema.Int;
27
+ }>;
28
+ export type StoredRequestMeasure = typeof StoredRequestMeasure.Type;
@@ -0,0 +1,15 @@
1
+ import { Option } from "effect";
2
+ import { MemoryObservation } from "@xandreed/core";
3
+ import type { AcceptedModelInput, ModelSelection, SessionLogEvent } from "@xandreed/core";
4
+ export declare const matchingObservation: (model: Option.Option<ModelSelection>, observed: Option.Option<MemoryObservation>) => Option.Option<MemoryObservation>;
5
+ export declare const observeAcceptedInput: (measurement: {
6
+ readonly estimatedTokens: number;
7
+ readonly reservedTokens: number;
8
+ }, accepted: AcceptedModelInput) => Option.Option<MemoryObservation>;
9
+ /**
10
+ * The last selected response among these stored events (`step.usage` with a
11
+ * model and a positive input), with the estimate and reserve its step's last
12
+ * `context.built` before it froze: accepted-input evidence that outlives the
13
+ * run that measured it. Rows that do not decode tell nothing.
14
+ */
15
+ export declare const acceptedInputOf: (events: ReadonlyArray<SessionLogEvent>) => Option.Option<MemoryObservation>;
@@ -0,0 +1,17 @@
1
+ import { Option, Schema } from "effect";
2
+ import { MemoryObservation, parseModelSelection, StepUsageData } from "@xandreed/core";
3
+ import { StoredRequestMeasure } from "./turn-observation.entity.js";
4
+ export const matchingObservation = (model, observed) => Option.filter(observed, (value) => Option.exists(model, (expected) => expected.provider === value.model.provider && expected.modelId === value.model.modelId)), observeAcceptedInput = (measurement, accepted) => Option.flatMap(Option.all({ model: accepted.model, inputTokens: accepted.inputTokens }), (actual) => Schema.decodeUnknownOption(MemoryObservation)({ ...actual, ...measurement }));
5
+ const decodeUsage = Schema.decodeUnknownOption(StepUsageData), decodeMeasure = Schema.decodeUnknownOption(StoredRequestMeasure);
6
+ export const acceptedInputOf = (events) => {
7
+ const usages = events.flatMap((event) => event.kind === "step.usage" ? Option.toArray(Option.map(decodeUsage(event.data), (data) => ({ event, data }))) : []), measures = events.flatMap((event) => event.kind === "context.built" ? Option.toArray(Option.map(decodeMeasure(event.data), (data) => ({ event, data }))) : []);
8
+ return Option.flatMap(Option.fromNullishOr(usages.filter(({ data }) => Option.isSome(data.model) && data.usage.inputTokens > 0).at(-1)), (last) => {
9
+ const measure = Option.fromNullishOr(measures.filter(({ event, data }) => event.seq < last.event.seq && Option.getOrElse(event.turn, () => -1) === Option.getOrElse(last.event.turn, () => -2) && data.run === last.data.run && data.step === last.data.step).at(-1)), model = Option.flatMap(last.data.model, parseModelSelection);
10
+ return Option.flatMap(Option.all({ measure, model }), ({ measure, model }) => Schema.decodeUnknownOption(MemoryObservation)({
11
+ model,
12
+ inputTokens: last.data.usage.inputTokens,
13
+ estimatedTokens: measure.data.estimatedTokens,
14
+ reservedTokens: measure.data.reservedTokens
15
+ }));
16
+ });
17
+ };
@@ -0,0 +1,12 @@
1
+ import { Schema } from "effect";
2
+ import { MemoryObservation, RunKey, TokenUsage } from "@xandreed/core";
3
+ export const TurnObservation = Schema.Struct({
4
+ observed: Schema.OptionFromNullOr(MemoryObservation),
5
+ usage: Schema.OptionFromNullOr(TokenUsage),
6
+ baseReservedTokens: Schema.OptionFromNullOr(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
7
+ }), StoredRequestMeasure = Schema.Struct({
8
+ step: Schema.Int,
9
+ run: RunKey,
10
+ estimatedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
11
+ reservedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
12
+ });
@@ -0,0 +1,7 @@
1
+ import { Schema } from "effect";
2
+ /** A strictly tighter input ceiling and the messages' share after fixed reserves. */
3
+ export declare const OverflowBudget: Schema.Struct<{
4
+ readonly inputCeiling: Schema.Int;
5
+ readonly messageBudget: Schema.Int;
6
+ }>;
7
+ export type OverflowBudget = typeof OverflowBudget.Type;
@@ -0,0 +1,11 @@
1
+ import { Option } from "effect";
2
+ import type { ModelCallPolicy, ModelLimits } from "@xandreed/core";
3
+ import { OverflowBudget } from "./turn-overflow.entity.js";
4
+ /** Output is an actual attempted cap when known; an absent cap stays unknown. */
5
+ export declare const tighterOverflowBudget: (previous: number, reserved: number, failedComparable: number, limits: ModelLimits) => Option.Option<OverflowBudget>;
6
+ /**
7
+ * A model's known input bound before any request: its catalog window less
8
+ * the output the request will reserve (the call policy's cap, else the
9
+ * catalog's). None without a known window or when nothing would remain.
10
+ */
11
+ export declare const windowBound: (limits: Option.Option<ModelLimits>, policy: Option.Option<ModelCallPolicy>) => Option.Option<number>;
@@ -0,0 +1,12 @@
1
+ import { Option, Schema } from "effect";
2
+ import { OverflowBudget } from "./turn-overflow.entity.js";
3
+ const positive = (value) => value !== void 0 && Number.isSafeInteger(value) && value > 0 ? Option.some(value) : Option.none();
4
+ export const tighterOverflowBudget = (previous, reserved, failedComparable, limits) => {
5
+ if (!Number.isSafeInteger(previous) || previous < 1 || !Number.isSafeInteger(reserved) || reserved < 0 || !Number.isSafeInteger(failedComparable) || failedComparable < 1)
6
+ return Option.none();
7
+ const context = positive(limits.contextWindow), output = positive(limits.maxOutputTokens), bound = Option.map(context, (window) => window - Option.getOrElse(output, () => 0)), applicable = Math.min(previous, Option.getOrElse(bound, () => previous)), messages = Math.floor(0.8 * Math.min(applicable - reserved, failedComparable));
8
+ return messages > 0 && messages < failedComparable && reserved + messages < previous ? Schema.decodeUnknownOption(OverflowBudget)({ inputCeiling: reserved + messages, messageBudget: messages }) : Option.none();
9
+ }, windowBound = (limits, policy) => {
10
+ const context = Option.flatMap(limits, (value) => positive(value.contextWindow)), output = Option.orElse(Option.flatMap(policy, (value) => positive(value.maxOutputTokens)), () => Option.flatMap(limits, (value) => positive(value.maxOutputTokens)));
11
+ return Option.filter(Option.map(context, (window) => window - Option.getOrElse(output, () => 0)), (bound) => bound > 0);
12
+ };
@@ -0,0 +1,5 @@
1
+ import { Schema } from "effect";
2
+ export const OverflowBudget = Schema.Struct({
3
+ inputCeiling: Schema.Int.check(Schema.isGreaterThan(0)),
4
+ messageBudget: Schema.Int.check(Schema.isGreaterThan(0))
5
+ });