@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,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
|
+
};
|