@effect-agent/ai-decision 0.1.0-beta.102

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Request and response schemas for provider-neutral decision evaluations.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Schema from "effect/Schema";
7
+
8
+ /**
9
+ * Text or JSON objects and arrays used as state and question instructions.
10
+ *
11
+ * @category schemas
12
+ * @since 0.1.0
13
+ */
14
+ export const Content = Schema.Union([Schema.String, Schema.JsonObject, Schema.Array(Schema.Json)]);
15
+
16
+ /** @category models
17
+ * @since 0.1.0
18
+ */
19
+ export type Content = typeof Content.Type;
20
+
21
+ /**
22
+ * A choice between named options. A null rubric uses the option name alone.
23
+ *
24
+ * @category schemas
25
+ * @since 0.1.0
26
+ */
27
+ export const ChoiceQuestion = Schema.Struct({
28
+ type: Schema.Literal("choice"),
29
+ instructions: Content,
30
+ criteria: Schema.Record(Schema.String, Schema.NullOr(Schema.String)).check(
31
+ Schema.isMinProperties(1),
32
+ ),
33
+ });
34
+
35
+ /** @category models
36
+ * @since 0.1.0
37
+ */
38
+ export type ChoiceQuestion = typeof ChoiceQuestion.Type;
39
+
40
+ /**
41
+ * A rating along at least two ordered, zero-indexed level descriptions.
42
+ *
43
+ * @category schemas
44
+ * @since 0.1.0
45
+ */
46
+ export const ScoreQuestion = Schema.Struct({
47
+ type: Schema.Literal("score"),
48
+ instructions: Content,
49
+ criteria: Schema.Array(Schema.String).check(Schema.isMinLength(2)),
50
+ });
51
+
52
+ /** @category models
53
+ * @since 0.1.0
54
+ */
55
+ export type ScoreQuestion = typeof ScoreQuestion.Type;
56
+
57
+ /**
58
+ * A yes/no judgment, with optional descriptions of either outcome.
59
+ *
60
+ * @category schemas
61
+ * @since 0.1.0
62
+ */
63
+ export const ProbabilityQuestion = Schema.Struct({
64
+ type: Schema.Literal("probability"),
65
+ instructions: Content,
66
+ criteria: Schema.optionalKey(
67
+ Schema.Struct({
68
+ true: Schema.optionalKey(Schema.String),
69
+ false: Schema.optionalKey(Schema.String),
70
+ }),
71
+ ),
72
+ });
73
+
74
+ /** @category models
75
+ * @since 0.1.0
76
+ */
77
+ export type ProbabilityQuestion = typeof ProbabilityQuestion.Type;
78
+
79
+ /** @category schemas
80
+ * @since 0.1.0
81
+ */
82
+ export const Question = Schema.Union([ChoiceQuestion, ScoreQuestion, ProbabilityQuestion]);
83
+
84
+ /** @category models
85
+ * @since 0.1.0
86
+ */
87
+ export type Question = typeof Question.Type;
88
+
89
+ /** @category schemas
90
+ * @since 0.1.0
91
+ */
92
+ export const Questions = Schema.Record(Schema.String, Question);
93
+
94
+ /** @category models
95
+ * @since 0.1.0
96
+ */
97
+ export type Questions = typeof Questions.Type;
98
+
99
+ /** @category schemas
100
+ * @since 0.1.0
101
+ */
102
+ export const EvaluateRequest = Schema.Struct({
103
+ state: Content,
104
+ questions: Questions,
105
+ });
106
+
107
+ /**
108
+ * Evaluation input. Keep question literals with `satisfies Questions` when
109
+ * storing questions separately from the call to `evaluate`.
110
+ *
111
+ * @category models
112
+ * @since 0.1.0
113
+ */
114
+ export type EvaluateRequest<Q extends Questions = Questions> = Omit<
115
+ typeof EvaluateRequest.Type,
116
+ "questions"
117
+ > & { readonly questions: Q };
118
+
119
+ /**
120
+ * A finite probability or confidence in the inclusive range [0, 1].
121
+ *
122
+ * @category schemas
123
+ * @since 0.1.0
124
+ */
125
+ export const Probability = Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 }));
126
+
127
+ /**
128
+ * A selected option and its full distribution. Provider-specific confidence
129
+ * statistics belong to evaluation metadata, not the shared answer contract.
130
+ * Probabilities retain the provider's reported precision and are not normalized.
131
+ *
132
+ * @category schemas
133
+ * @since 0.1.0
134
+ */
135
+ export const ChoiceAnswer = Schema.Struct({
136
+ type: Schema.Literal("choice"),
137
+ choice: Schema.String,
138
+ probabilities: Schema.Record(Schema.String, Probability),
139
+ });
140
+
141
+ /**
142
+ * Known option unions infer literal choices and required probability keys.
143
+ * Open string or template-pattern option types allow absent dictionary entries.
144
+ *
145
+ * @category models
146
+ * @since 0.1.0
147
+ */
148
+ export type ChoiceAnswer<Choice extends string = string> = Omit<
149
+ typeof ChoiceAnswer.Type,
150
+ "choice" | "probabilities"
151
+ > & {
152
+ readonly choice: Choice;
153
+ readonly probabilities: {
154
+ readonly [K in Choice]: {} extends Pick<Record<Choice, unknown>, K>
155
+ ? number | undefined
156
+ : number;
157
+ };
158
+ };
159
+
160
+ /**
161
+ * A fractional, probability-weighted level, with the supplied rubric as legend.
162
+ *
163
+ * @category schemas
164
+ * @since 0.1.0
165
+ */
166
+ export const ScoreAnswer = Schema.Struct({
167
+ type: Schema.Literal("score"),
168
+ score: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
169
+ legend: Schema.Record(Schema.String, Schema.String),
170
+ probabilities: Schema.Record(Schema.String, Probability),
171
+ });
172
+
173
+ /**
174
+ * Score levels are keyed at runtime; an arbitrary legend or probability lookup
175
+ * may be absent.
176
+ *
177
+ * @category models
178
+ * @since 0.1.0
179
+ */
180
+ export type ScoreAnswer = Omit<typeof ScoreAnswer.Type, "legend" | "probabilities"> & {
181
+ readonly legend: { readonly [level: string]: string | undefined };
182
+ readonly probabilities: { readonly [level: string]: number | undefined };
183
+ };
184
+
185
+ /**
186
+ * The probability of yes. A probability answer has no separate confidence field.
187
+ *
188
+ * @category schemas
189
+ * @since 0.1.0
190
+ */
191
+ export const ProbabilityAnswer = Schema.Struct({
192
+ type: Schema.Literal("probability"),
193
+ probability: Probability,
194
+ });
195
+
196
+ /** @category models
197
+ * @since 0.1.0
198
+ */
199
+ export type ProbabilityAnswer = typeof ProbabilityAnswer.Type;
200
+
201
+ /** @category schemas
202
+ * @since 0.1.0
203
+ */
204
+ export const Answer = Schema.Union([ChoiceAnswer, ScoreAnswer, ProbabilityAnswer]);
205
+
206
+ /** @category models
207
+ * @since 0.1.0
208
+ */
209
+ export type Answer = ChoiceAnswer | ScoreAnswer | ProbabilityAnswer;
210
+
211
+ /** @category schemas
212
+ * @since 0.1.0
213
+ */
214
+ export const Usage = Schema.Struct({
215
+ inputTokens: Schema.NullOr(Schema.Natural),
216
+ outputTokens: Schema.NullOr(Schema.Natural),
217
+ });
218
+
219
+ /** @category models
220
+ * @since 0.1.0
221
+ */
222
+ export type Usage = typeof Usage.Type;
223
+
224
+ /**
225
+ * Provider-namespaced evidence that has no shared interpretation. Consumers
226
+ * decode a namespace with its provider's schema before using its contents.
227
+ *
228
+ * @category schemas
229
+ * @since 0.1.0
230
+ */
231
+ export const ProviderMetadata = Schema.Record(Schema.String, Schema.JsonObject);
232
+
233
+ /** @category models
234
+ * @since 0.1.0
235
+ */
236
+ export type ProviderMetadata = typeof ProviderMetadata.Type;
237
+
238
+ /**
239
+ * The wire response shape. `DecisionModel.evaluate` additionally validates
240
+ * answer IDs, question kinds, criteria, distributions, and score correlations.
241
+ *
242
+ * @category schemas
243
+ * @since 0.1.0
244
+ */
245
+ export const EvaluateResponse = Schema.Struct({
246
+ provider: Schema.NonEmptyString,
247
+ model: Schema.String,
248
+ answers: Schema.Record(Schema.String, Answer),
249
+ usage: Usage,
250
+ providerMetadata: Schema.optionalKey(ProviderMetadata),
251
+ });
252
+
253
+ type ChoiceAnswerFor<Criteria> = Criteria extends unknown
254
+ ? Omit<typeof ChoiceAnswer.Type, "choice" | "probabilities"> & {
255
+ readonly choice: `${Extract<keyof Criteria, string | number>}`;
256
+ readonly probabilities: {
257
+ readonly [
258
+ K in keyof Criteria as K extends string | number ? `${K}` : never
259
+ ]: {} extends Pick<Criteria, K> ? number | undefined : number;
260
+ };
261
+ }
262
+ : never;
263
+
264
+ /**
265
+ * Infer a question's answer, preserving optional choice criteria in its probabilities.
266
+ *
267
+ * @category models
268
+ * @since 0.1.0
269
+ */
270
+ export type AnswerFor<Q extends Question> = Q extends ChoiceQuestion
271
+ ? ChoiceAnswerFor<Q["criteria"]>
272
+ : Q extends ScoreQuestion
273
+ ? ScoreAnswer
274
+ : ProbabilityAnswer;
275
+
276
+ /**
277
+ * One answer for each required question. Optional properties and open string,
278
+ * numeric, or template-pattern indexes require checking an entry for absence.
279
+ *
280
+ * @category models
281
+ * @since 0.1.0
282
+ */
283
+ export type Answers<Q extends Questions> = {
284
+ readonly [K in keyof Q as K extends string | number ? `${K}` : never]: {} extends Pick<Q, K>
285
+ ? AnswerFor<NonNullable<Q[K]>> | undefined
286
+ : AnswerFor<Q[K]>;
287
+ };
288
+
289
+ /** @category models
290
+ * @since 0.1.0
291
+ */
292
+ export type EvaluateResponse<Q extends Questions = Questions> = Omit<
293
+ typeof EvaluateResponse.Type,
294
+ "answers"
295
+ > & { readonly answers: Answers<Q> };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Reusable question collections with a schema-defined input. The set owns no
3
+ * provider, live resources, routing policy, or execution state.
4
+ *
5
+ * @since 0.1.0
6
+ */
7
+ import type * as Schema from "effect/Schema";
8
+
9
+ import type * as DecisionSchema from "./DecisionSchema.ts";
10
+
11
+ /** @category models
12
+ * @since 0.1.0
13
+ */
14
+ export interface DecisionSet<Input extends Schema.Top, Questions extends DecisionSchema.Questions> {
15
+ /** The encoded input becomes model-visible state: a string, JSON object, or JSON array. */
16
+ readonly input: Input;
17
+ /** Independent questions evaluated together against that state. */
18
+ readonly questions: Questions;
19
+ }
20
+
21
+ /**
22
+ * Describe a reusable assessment. Construction performs no encoding or model
23
+ * I/O. Input encoding and question validation occur inside model.evaluate.
24
+ * Treat nested instruction content as readonly; evaluation snapshots it before
25
+ * calling the provider. Dynamic question records are supported.
26
+ *
27
+ * @category constructors
28
+ * @since 0.1.0
29
+ */
30
+ export const make = <Input extends Schema.Top, const Questions extends DecisionSchema.Questions>(
31
+ options: DecisionSet<Input, Questions>,
32
+ ): DecisionSet<Input, Questions> =>
33
+ Object.freeze({
34
+ input: options.input,
35
+ questions: Object.freeze({ ...options.questions }),
36
+ });
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * as DecisionModel from "./DecisionModel.ts";
2
+ export * as DecisionQuery from "./DecisionQuery.ts";
3
+ export * as DecisionSchema from "./DecisionSchema.ts";
4
+ export * as DecisionSet from "./DecisionSet.ts";
@@ -0,0 +1,98 @@
1
+ import * as Schema from "effect/Schema";
2
+ import type * as SchemaAST from "effect/SchemaAST";
3
+
4
+ import * as DecisionSchema from "../DecisionSchema.ts";
5
+
6
+ // Permit floating-point serialization error without changing provider values.
7
+ const tolerance = 1e-6;
8
+
9
+ const probabilitySum = Schema.makeFilter(
10
+ (probabilities: Readonly<Record<string, number>>) =>
11
+ Math.abs(Object.values(probabilities).reduce((sum, value) => sum + value, 0) - 1) <= tolerance,
12
+ { expected: "probabilities summing to 1 (within 1e-6)" },
13
+ );
14
+
15
+ const distribution = (
16
+ keys: ReadonlyArray<string>,
17
+ sumCheck: SchemaAST.Check<Readonly<Record<string, number>>> = probabilitySum,
18
+ ) => Schema.Record(Schema.Literals(keys), DecisionSchema.Probability).check(sumCheck);
19
+
20
+ const answerFor = (
21
+ question: DecisionSchema.Question,
22
+ choiceProbabilitySum: SchemaAST.Check<Readonly<Record<string, number>>> | undefined,
23
+ ) => {
24
+ switch (question.type) {
25
+ case "choice": {
26
+ const keys = Object.keys(question.criteria);
27
+
28
+ return Schema.Struct({
29
+ ...DecisionSchema.ChoiceAnswer.fields,
30
+ choice: Schema.Literals(keys),
31
+ probabilities: distribution(keys, choiceProbabilitySum),
32
+ }).check(
33
+ Schema.makeFilter(
34
+ ({ choice, probabilities }) =>
35
+ Object.values(probabilities).every((value) => value <= probabilities[choice]),
36
+ { expected: "a highest-probability choice" },
37
+ ),
38
+ );
39
+ }
40
+ case "score": {
41
+ const maxLevel = question.criteria.length - 1;
42
+
43
+ const levels = question.criteria.map(
44
+ (description, index) => [String(index), description] as const,
45
+ );
46
+
47
+ return Schema.Struct({
48
+ ...DecisionSchema.ScoreAnswer.fields,
49
+ score: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: maxLevel })),
50
+ legend: Schema.Struct(
51
+ Object.fromEntries(
52
+ levels.map(([key, description]) => [key, Schema.Literal(description)]),
53
+ ),
54
+ ),
55
+ probabilities: distribution(levels.map(([key]) => key)),
56
+ }).check(
57
+ Schema.makeFilter(
58
+ ({ score, probabilities }) =>
59
+ Math.abs(
60
+ score -
61
+ Object.entries(probabilities).reduce(
62
+ (sum, [level, probability]) => sum + Number(level) * probability,
63
+ 0,
64
+ ),
65
+ ) <=
66
+ tolerance * Math.max(1, maxLevel),
67
+ { expected: "the probability-weighted score (within 1e-6 per level)" },
68
+ ),
69
+ );
70
+ }
71
+ case "probability":
72
+ return DecisionSchema.ProbabilityAnswer;
73
+ }
74
+ };
75
+
76
+ // The overload describes the dependent type enforced by the literal keys and
77
+ // per-question schemas below. No JSON value is asserted to have that type.
78
+ export function responseFor<const Q extends DecisionSchema.Questions>(
79
+ questions: Q,
80
+ choiceProbabilitySum?: SchemaAST.Check<Readonly<Record<string, number>>>,
81
+ ): Schema.Codec<DecisionSchema.EvaluateResponse<Q>>;
82
+
83
+ export function responseFor(
84
+ questions: DecisionSchema.Questions,
85
+ choiceProbabilitySum?: SchemaAST.Check<Readonly<Record<string, number>>>,
86
+ ): Schema.Top {
87
+ return Schema.Struct({
88
+ ...DecisionSchema.EvaluateResponse.fields,
89
+ answers: Schema.Struct(
90
+ Object.fromEntries(
91
+ Object.entries(questions).map(([id, question]) => [
92
+ id,
93
+ answerFor(question, choiceProbabilitySum),
94
+ ]),
95
+ ),
96
+ ),
97
+ });
98
+ }