@effect-uai/core 0.1.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/AiError-CqmYjXyx.d.mts +110 -0
- package/dist/AiError-CqmYjXyx.d.mts.map +1 -0
- package/dist/Items-D1C2686t.d.mts +372 -0
- package/dist/Items-D1C2686t.d.mts.map +1 -0
- package/dist/Loop-CzSJo1h8.d.mts +87 -0
- package/dist/Loop-CzSJo1h8.d.mts.map +1 -0
- package/dist/Outcome-C2JYknCu.d.mts +40 -0
- package/dist/Outcome-C2JYknCu.d.mts.map +1 -0
- package/dist/StructuredFormat-B5ueioNr.d.mts +88 -0
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +1 -0
- package/dist/Tool-5wxOCuOh.d.mts +86 -0
- package/dist/Tool-5wxOCuOh.d.mts.map +1 -0
- package/dist/ToolEvent-B2N10hr3.d.mts +29 -0
- package/dist/ToolEvent-B2N10hr3.d.mts.map +1 -0
- package/dist/Turn-rlTfuHaQ.d.mts +211 -0
- package/dist/Turn-rlTfuHaQ.d.mts.map +1 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/domain/AiError.d.mts +2 -0
- package/dist/domain/AiError.mjs +40 -0
- package/dist/domain/AiError.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -0
- package/dist/domain/Items.mjs +238 -0
- package/dist/domain/Items.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +2 -0
- package/dist/domain/Turn.mjs +82 -0
- package/dist/domain/Turn.mjs.map +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +14 -0
- package/dist/language-model/LanguageModel.d.mts +60 -0
- package/dist/language-model/LanguageModel.d.mts.map +1 -0
- package/dist/language-model/LanguageModel.mjs +33 -0
- package/dist/language-model/LanguageModel.mjs.map +1 -0
- package/dist/loop/Loop.d.mts +2 -0
- package/dist/loop/Loop.mjs +172 -0
- package/dist/loop/Loop.mjs.map +1 -0
- package/dist/match/Match.d.mts +16 -0
- package/dist/match/Match.d.mts.map +1 -0
- package/dist/match/Match.mjs +15 -0
- package/dist/match/Match.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +45 -0
- package/dist/observability/Metrics.d.mts.map +1 -0
- package/dist/observability/Metrics.mjs +52 -0
- package/dist/observability/Metrics.mjs.map +1 -0
- package/dist/streaming/JSONL.d.mts +34 -0
- package/dist/streaming/JSONL.d.mts.map +1 -0
- package/dist/streaming/JSONL.mjs +51 -0
- package/dist/streaming/JSONL.mjs.map +1 -0
- package/dist/streaming/Lines.d.mts +27 -0
- package/dist/streaming/Lines.d.mts.map +1 -0
- package/dist/streaming/Lines.mjs +32 -0
- package/dist/streaming/Lines.mjs.map +1 -0
- package/dist/streaming/SSE.d.mts +31 -0
- package/dist/streaming/SSE.d.mts.map +1 -0
- package/dist/streaming/SSE.mjs +58 -0
- package/dist/streaming/SSE.mjs.map +1 -0
- package/dist/structured-format/StructuredFormat.d.mts +2 -0
- package/dist/structured-format/StructuredFormat.mjs +68 -0
- package/dist/structured-format/StructuredFormat.mjs.map +1 -0
- package/dist/testing/MockProvider.d.mts +48 -0
- package/dist/testing/MockProvider.d.mts.map +1 -0
- package/dist/testing/MockProvider.mjs +95 -0
- package/dist/testing/MockProvider.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +24 -0
- package/dist/tool/HistoryCheck.d.mts.map +1 -0
- package/dist/tool/HistoryCheck.mjs +39 -0
- package/dist/tool/HistoryCheck.mjs.map +1 -0
- package/dist/tool/Outcome.d.mts +2 -0
- package/dist/tool/Outcome.mjs +45 -0
- package/dist/tool/Outcome.mjs.map +1 -0
- package/dist/tool/Resolvers.d.mts +44 -0
- package/dist/tool/Resolvers.d.mts.map +1 -0
- package/dist/tool/Resolvers.mjs +67 -0
- package/dist/tool/Resolvers.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +2 -0
- package/dist/tool/Tool.mjs +79 -0
- package/dist/tool/Tool.mjs.map +1 -0
- package/dist/tool/ToolEvent.d.mts +2 -0
- package/dist/tool/ToolEvent.mjs +8 -0
- package/dist/tool/ToolEvent.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +34 -0
- package/dist/tool/Toolkit.d.mts.map +1 -0
- package/dist/tool/Toolkit.mjs +105 -0
- package/dist/tool/Toolkit.mjs.map +1 -0
- package/package.json +127 -0
- package/src/domain/AiError.ts +93 -0
- package/src/domain/Items.ts +260 -0
- package/src/domain/Turn.ts +174 -0
- package/src/index.ts +13 -0
- package/src/language-model/LanguageModel.ts +73 -0
- package/src/loop/Loop.test.ts +412 -0
- package/src/loop/Loop.ts +295 -0
- package/src/match/Match.ts +9 -0
- package/src/observability/Metrics.ts +87 -0
- package/src/streaming/JSONL.test.ts +85 -0
- package/src/streaming/JSONL.ts +96 -0
- package/src/streaming/Lines.ts +34 -0
- package/src/streaming/SSE.test.ts +72 -0
- package/src/streaming/SSE.ts +114 -0
- package/src/structured-format/StructuredFormat.ts +160 -0
- package/src/testing/MockProvider.ts +161 -0
- package/src/tool/HistoryCheck.ts +49 -0
- package/src/tool/Outcome.ts +101 -0
- package/src/tool/Resolvers.test.ts +426 -0
- package/src/tool/Resolvers.ts +166 -0
- package/src/tool/Tool.ts +150 -0
- package/src/tool/ToolEvent.ts +37 -0
- package/src/tool/Toolkit.test.ts +45 -0
- package/src/tool/Toolkit.ts +228 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SSE.mjs","names":[],"sources":["../../src/streaming/SSE.ts"],"sourcesContent":["import { Stream } from \"effect\"\n\n/**\n * One Server-Sent Event. Fields per the WHATWG spec:\n * - `event`: optional event name (default \"message\" on the wire)\n * - `data`: payload, with multiple `data:` lines joined by `\\n`\n * - `id`: optional last-event id\n */\nexport interface Event {\n readonly event?: string\n readonly data: string\n readonly id?: string\n}\n\n// ---------------------------------------------------------------------------\n// Generic stream helpers (kept module-local for now; promote to a shared\n// Stream module once a third caller appears).\n// ---------------------------------------------------------------------------\n\n/** Decode `Uint8Array` chunks as UTF-8, handling multi-byte boundaries. */\nconst decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): TextDecoder => new TextDecoder(\"utf-8\"),\n (decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,\n {\n onHalt: (decoder: TextDecoder) => {\n const tail = decoder.decode()\n return tail.length > 0 ? [tail] : []\n },\n },\n ),\n )\n\n/** Split a text stream on a separator, buffering across chunk boundaries. */\nconst splitOn =\n (separator: string) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): string => \"\",\n (buffer, chunk: string) => {\n const parts = (buffer + chunk).split(separator)\n const tail = parts[parts.length - 1] ?? \"\"\n return [tail, parts.slice(0, -1)] as const\n },\n { onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Parser\n// ---------------------------------------------------------------------------\n\nconst parseField = (line: string): readonly [string, string] => {\n const colon = line.indexOf(\":\")\n if (colon < 0) return [line, \"\"]\n const value = line.slice(colon + 1)\n return [line.slice(0, colon), value.startsWith(\" \") ? value.slice(1) : value]\n}\n\nconst parseBlock = (block: string): Event | null => {\n const lines = block.split(\"\\n\").filter((l) => l.length > 0 && !l.startsWith(\":\"))\n if (lines.length === 0) return null\n\n const fields = lines.map(parseField)\n const dataLines = fields.filter(([f]) => f === \"data\").map(([, v]) => v)\n const event = fields.find(([f]) => f === \"event\")?.[1]\n const id = fields.find(([f]) => f === \"id\")?.[1]\n\n const out: { event?: string; data: string; id?: string } = {\n data: dataLines.join(\"\\n\"),\n }\n if (event !== undefined) out.event = event\n if (id !== undefined) out.id = id\n return out as Event\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a\n * `Stream<SSE.Event>`. Handles partial UTF-8 sequences, CRLF/LF line\n * endings, and events split across chunk boundaries.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<Event, E, R> =>\n self.pipe(\n decodeText,\n Stream.map((s) => s.replace(/\\r/g, \"\")), // SSE allows CRLF; normalize to LF\n splitOn(\"\\n\\n\"),\n Stream.map(parseBlock),\n Stream.filter((ev): ev is Event => ev !== null),\n )\n\nconst eventToString = (ev: Event): string => {\n const parts: string[] = []\n if (ev.event !== undefined) parts.push(`event: ${ev.event}`)\n if (ev.id !== undefined) parts.push(`id: ${ev.id}`)\n for (const line of ev.data.split(\"\\n\")) parts.push(`data: ${line}`)\n return parts.join(\"\\n\") + \"\\n\\n\"\n}\n\nconst encoder = new TextEncoder()\n\n/**\n * Encode a `Stream<Event>` as `Stream<Uint8Array>` ready to send on an\n * HTTP response with `Content-Type: text/event-stream`.\n */\nexport const toBytes = <E, R>(self: Stream.Stream<Event, E, R>): Stream.Stream<Uint8Array, E, R> =>\n Stream.map(self, (ev) => encoder.encode(eventToString(ev)))\n"],"mappings":";;;;;;;;AAoBA,MAAM,cAAoB,SACxB,KAAK,KACH,OAAO,eACc,IAAI,YAAY,QAAQ,GAC1C,SAAS,UAAsB,CAAC,SAAS,CAAC,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,EACpF,EACE,SAAS,YAAyB;CAChC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAO,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE;GAEvC,CACF,CACF;;AAGH,MAAM,WACH,eACM,SACL,KAAK,KACH,OAAO,eACS,KACb,QAAQ,UAAkB;CACzB,MAAM,SAAS,SAAS,OAAO,MAAM,UAAU;AAE/C,QAAO,CADM,MAAM,MAAM,SAAS,MAAM,IAC1B,MAAM,MAAM,GAAG,GAAG,CAAC;GAEnC,EAAE,SAAS,SAAkB,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE,EAAG,CAC9D,CACF;AAML,MAAM,cAAc,SAA4C;CAC9D,MAAM,QAAQ,KAAK,QAAQ,IAAI;AAC/B,KAAI,QAAQ,EAAG,QAAO,CAAC,MAAM,GAAG;CAChC,MAAM,QAAQ,KAAK,MAAM,QAAQ,EAAE;AACnC,QAAO,CAAC,KAAK,MAAM,GAAG,MAAM,EAAE,MAAM,WAAW,IAAI,GAAG,MAAM,MAAM,EAAE,GAAG,MAAM;;AAG/E,MAAM,cAAc,UAAgC;CAClD,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,QAAQ,MAAM,EAAE,SAAS,KAAK,CAAC,EAAE,WAAW,IAAI,CAAC;AACjF,KAAI,MAAM,WAAW,EAAG,QAAO;CAE/B,MAAM,SAAS,MAAM,IAAI,WAAW;CACpC,MAAM,YAAY,OAAO,QAAQ,CAAC,OAAO,MAAM,OAAO,CAAC,KAAK,GAAG,OAAO,EAAE;CACxE,MAAM,QAAQ,OAAO,MAAM,CAAC,OAAO,MAAM,QAAQ,GAAG;CACpD,MAAM,KAAK,OAAO,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG;CAE9C,MAAM,MAAqD,EACzD,MAAM,UAAU,KAAK,KAAK,EAC3B;AACD,KAAI,UAAU,KAAA,EAAW,KAAI,QAAQ;AACrC,KAAI,OAAO,KAAA,EAAW,KAAI,KAAK;AAC/B,QAAO;;;;;;;AAYT,MAAa,aACX,SAEA,KAAK,KACH,YACA,OAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,QAAQ,OAAO,EACf,OAAO,IAAI,WAAW,EACtB,OAAO,QAAQ,OAAoB,OAAO,KAAK,CAChD;AAEH,MAAM,iBAAiB,OAAsB;CAC3C,MAAM,QAAkB,EAAE;AAC1B,KAAI,GAAG,UAAU,KAAA,EAAW,OAAM,KAAK,UAAU,GAAG,QAAQ;AAC5D,KAAI,GAAG,OAAO,KAAA,EAAW,OAAM,KAAK,OAAO,GAAG,KAAK;AACnD,MAAK,MAAM,QAAQ,GAAG,KAAK,MAAM,KAAK,CAAE,OAAM,KAAK,SAAS,OAAO;AACnE,QAAO,MAAM,KAAK,KAAK,GAAG;;AAG5B,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WAAiB,SAC5B,OAAO,IAAI,OAAO,OAAO,QAAQ,OAAO,cAAc,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { c as decodeJsonLines, i as StructuredFormat, l as fromEffectSchema, n as JsonParseError, o as StructuredSchema, r as StructuredDecodeError, s as decode, t as DecodeIssue, u as parseJson } from "../StructuredFormat-B5ueioNr.mjs";
|
|
2
|
+
export { DecodeIssue, JsonParseError, StructuredDecodeError, StructuredFormat, StructuredSchema, decode, decodeJsonLines, fromEffectSchema, parseJson };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { Data, Effect, Match, Schema, Stream, pipe } from "effect";
|
|
3
|
+
//#region src/structured-format/StructuredFormat.ts
|
|
4
|
+
var StructuredFormat_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
JsonParseError: () => JsonParseError,
|
|
6
|
+
StructuredDecodeError: () => StructuredDecodeError,
|
|
7
|
+
decode: () => decode,
|
|
8
|
+
decodeJsonLines: () => decodeJsonLines,
|
|
9
|
+
fromEffectSchema: () => fromEffectSchema,
|
|
10
|
+
parseJson: () => parseJson
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Schema validation failed. `raw` is the original text (or stringified
|
|
14
|
+
* value) that failed; `issues` is a flat list of per-field problems.
|
|
15
|
+
*/
|
|
16
|
+
var StructuredDecodeError = class extends Data.TaggedError("StructuredDecodeError") {};
|
|
17
|
+
/**
|
|
18
|
+
* `JSON.parse` threw on a string that was supposed to be JSON. Distinct
|
|
19
|
+
* from `StructuredDecodeError`: the bytes weren't even JSON.
|
|
20
|
+
*/
|
|
21
|
+
var JsonParseError = class extends Data.TaggedError("StructuredJsonParseError") {};
|
|
22
|
+
/**
|
|
23
|
+
* Wrap an Effect `Schema` as a `StructuredFormat`. Effect Schema doesn't
|
|
24
|
+
* natively implement Standard Schema; this helper installs the
|
|
25
|
+
* `~standard` and JSON Schema interfaces.
|
|
26
|
+
*/
|
|
27
|
+
const fromEffectSchema = (schema, options) => ({
|
|
28
|
+
name: options?.name ?? "output",
|
|
29
|
+
schema: Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)),
|
|
30
|
+
...options?.description !== void 0 && { description: options.description },
|
|
31
|
+
...options?.strict !== void 0 && { strict: options.strict }
|
|
32
|
+
});
|
|
33
|
+
const propertyKeyToScalar = Match.type().pipe(Match.when(Match.string, (s) => s), Match.when(Match.number, (n) => n), Match.when(Match.symbol, (s) => s.toString()), Match.exhaustive);
|
|
34
|
+
const segmentToKey = Match.type().pipe(Match.when(Match.string, (s) => s), Match.when(Match.number, (n) => n), Match.when(Match.symbol, (s) => s.toString()), Match.orElse((segment) => propertyKeyToScalar(segment.key)));
|
|
35
|
+
const issueToDecode = (issue) => ({
|
|
36
|
+
path: (issue.path ?? []).map(segmentToKey),
|
|
37
|
+
message: issue.message
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Validate an `unknown` against the format's schema. Returns the typed
|
|
41
|
+
* value or a `StructuredDecodeError`. Standard Schema's `validate` may
|
|
42
|
+
* be async; this function handles both sync and async results.
|
|
43
|
+
*/
|
|
44
|
+
const decode = (format) => (raw) => pipe(Effect.promise(async () => format.schema["~standard"].validate(raw)), Effect.flatMap((result) => result.issues === void 0 ? Effect.succeed(result.value) : Effect.fail(new StructuredDecodeError({
|
|
45
|
+
raw: typeof raw === "string" ? raw : JSON.stringify(raw),
|
|
46
|
+
issues: result.issues.map(issueToDecode)
|
|
47
|
+
}))));
|
|
48
|
+
/**
|
|
49
|
+
* Parse a JSON string then validate against the format's schema. Two
|
|
50
|
+
* failure modes: `JsonParseError` (bytes weren't JSON) and
|
|
51
|
+
* `StructuredDecodeError` (JSON didn't match the schema).
|
|
52
|
+
*/
|
|
53
|
+
const parseJson = (format) => (raw) => pipe(Effect.try({
|
|
54
|
+
try: () => JSON.parse(raw),
|
|
55
|
+
catch: (cause) => new JsonParseError({
|
|
56
|
+
raw,
|
|
57
|
+
cause
|
|
58
|
+
})
|
|
59
|
+
}), Effect.flatMap(decode(format)));
|
|
60
|
+
/**
|
|
61
|
+
* Stream operator: each input string is JSON-parsed and validated.
|
|
62
|
+
* Failures surface in the stream's failure channel, distinguished by tag.
|
|
63
|
+
*/
|
|
64
|
+
const decodeJsonLines = (format) => (self) => self.pipe(Stream.mapEffect(parseJson(format)));
|
|
65
|
+
//#endregion
|
|
66
|
+
export { JsonParseError, StructuredDecodeError, decode, decodeJsonLines, fromEffectSchema, parseJson, StructuredFormat_exports as t };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=StructuredFormat.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StructuredFormat.mjs","names":[],"sources":["../../src/structured-format/StructuredFormat.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Data, Effect, Match, Schema, Stream, pipe } from \"effect\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-validator schema constraint for structured outputs. Any schema\n * implementing both Standard Schema (runtime validation) and Standard\n * JSON Schema (wire encoding) works directly: Zod 4+, Valibot, ArkType,\n * and Effect Schema after `fromEffectSchema`.\n */\nexport type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Output> &\n StandardJSONSchemaV1<unknown, Output>\n\n/**\n * A schema-bound output the user wants the model to produce. Pairs the\n * cross-validator schema with metadata providers need (name, description,\n * strict-mode flag).\n */\nexport interface StructuredFormat<A> {\n readonly name: string\n readonly description?: string\n readonly schema: StructuredSchema<A>\n /**\n * Provider strict-mode flag. OpenAI, Anthropic, and Mistral honour it\n * (constrained decoding); other providers ignore.\n */\n readonly strict?: boolean\n}\n\n/** A single path-scoped validation problem. Library-agnostic shape. */\nexport interface DecodeIssue {\n readonly path: ReadonlyArray<string | number>\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * Schema validation failed. `raw` is the original text (or stringified\n * value) that failed; `issues` is a flat list of per-field problems.\n */\nexport class StructuredDecodeError extends Data.TaggedError(\"StructuredDecodeError\")<{\n readonly raw: string\n readonly issues: ReadonlyArray<DecodeIssue>\n}> {}\n\n/**\n * `JSON.parse` threw on a string that was supposed to be JSON. Distinct\n * from `StructuredDecodeError`: the bytes weren't even JSON.\n */\nexport class JsonParseError extends Data.TaggedError(\"StructuredJsonParseError\")<{\n readonly raw: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap an Effect `Schema` as a `StructuredFormat`. Effect Schema doesn't\n * natively implement Standard Schema; this helper installs the\n * `~standard` and JSON Schema interfaces.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n options?: {\n readonly name?: string\n readonly description?: string\n readonly strict?: boolean\n },\n): StructuredFormat<S[\"Type\"]> => ({\n name: options?.name ?? \"output\",\n schema: Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)),\n ...(options?.description !== undefined && {\n description: options.description,\n }),\n ...(options?.strict !== undefined && { strict: options.strict }),\n})\n\n// ---------------------------------------------------------------------------\n// Standard Schema → DecodeIssue\n// ---------------------------------------------------------------------------\n\nconst propertyKeyToScalar = Match.type<PropertyKey>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.exhaustive,\n)\n\nconst segmentToKey = Match.type<PropertyKey | StandardSchemaV1.PathSegment>().pipe(\n Match.when(Match.string, (s) => s),\n Match.when(Match.number, (n) => n),\n Match.when(Match.symbol, (s) => s.toString()),\n Match.orElse((segment) => propertyKeyToScalar(segment.key)),\n)\n\nconst issueToDecode = (issue: StandardSchemaV1.Issue): DecodeIssue => ({\n path: (issue.path ?? []).map(segmentToKey),\n message: issue.message,\n})\n\n// ---------------------------------------------------------------------------\n// Decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Validate an `unknown` against the format's schema. Returns the typed\n * value or a `StructuredDecodeError`. Standard Schema's `validate` may\n * be async; this function handles both sync and async results.\n */\nexport const decode =\n <A>(format: StructuredFormat<A>) =>\n (raw: unknown): Effect.Effect<A, StructuredDecodeError> =>\n pipe(\n Effect.promise(async () => format.schema[\"~standard\"].validate(raw)),\n Effect.flatMap((result) =>\n result.issues === undefined\n ? Effect.succeed(result.value)\n : Effect.fail(\n new StructuredDecodeError({\n raw: typeof raw === \"string\" ? raw : JSON.stringify(raw),\n issues: result.issues.map(issueToDecode),\n }),\n ),\n ),\n )\n\n/**\n * Parse a JSON string then validate against the format's schema. Two\n * failure modes: `JsonParseError` (bytes weren't JSON) and\n * `StructuredDecodeError` (JSON didn't match the schema).\n */\nexport const parseJson =\n <A>(format: StructuredFormat<A>) =>\n (raw: string): Effect.Effect<A, JsonParseError | StructuredDecodeError> =>\n pipe(\n Effect.try({\n try: () => JSON.parse(raw),\n catch: (cause) => new JsonParseError({ raw, cause }),\n }),\n Effect.flatMap(decode(format)),\n )\n\n/**\n * Stream operator: each input string is JSON-parsed and validated.\n * Failures surface in the stream's failure channel, distinguished by tag.\n */\nexport const decodeJsonLines =\n <A>(format: StructuredFormat<A>) =>\n <E, R>(\n self: Stream.Stream<string, E, R>,\n ): Stream.Stream<A, E | JsonParseError | StructuredDecodeError, R> =>\n self.pipe(Stream.mapEffect(parseJson(format)))\n"],"mappings":";;;;;;;;;;;;;;;AA8CA,IAAa,wBAAb,cAA2C,KAAK,YAAY,wBAAwB,CAGjF;;;;;AAMH,IAAa,iBAAb,cAAoC,KAAK,YAAY,2BAA2B,CAG7E;;;;;;AAWH,MAAa,oBACX,QACA,aAKiC;CACjC,MAAM,SAAS,QAAQ;CACvB,QAAQ,OAAO,uBAAuB,OAAO,mBAAmB,OAAO,CAAC;CACxE,GAAI,SAAS,gBAAgB,KAAA,KAAa,EACxC,aAAa,QAAQ,aACtB;CACD,GAAI,SAAS,WAAW,KAAA,KAAa,EAAE,QAAQ,QAAQ,QAAQ;CAChE;AAMD,MAAM,sBAAsB,MAAM,MAAmB,CAAC,KACpD,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,UAAU,CAAC,EAC7C,MAAM,WACP;AAED,MAAM,eAAe,MAAM,MAAkD,CAAC,KAC5E,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,EAClC,MAAM,KAAK,MAAM,SAAS,MAAM,EAAE,UAAU,CAAC,EAC7C,MAAM,QAAQ,YAAY,oBAAoB,QAAQ,IAAI,CAAC,CAC5D;AAED,MAAM,iBAAiB,WAAgD;CACrE,OAAO,MAAM,QAAQ,EAAE,EAAE,IAAI,aAAa;CAC1C,SAAS,MAAM;CAChB;;;;;;AAWD,MAAa,UACP,YACH,QACC,KACE,OAAO,QAAQ,YAAY,OAAO,OAAO,aAAa,SAAS,IAAI,CAAC,EACpE,OAAO,SAAS,WACd,OAAO,WAAW,KAAA,IACd,OAAO,QAAQ,OAAO,MAAM,GAC5B,OAAO,KACL,IAAI,sBAAsB;CACxB,KAAK,OAAO,QAAQ,WAAW,MAAM,KAAK,UAAU,IAAI;CACxD,QAAQ,OAAO,OAAO,IAAI,cAAc;CACzC,CAAC,CACH,CACN,CACF;;;;;;AAOL,MAAa,aACP,YACH,QACC,KACE,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,IAAI;CAC1B,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAK;EAAO,CAAC;CACrD,CAAC,EACF,OAAO,QAAQ,OAAO,OAAO,CAAC,CAC/B;;;;;AAML,MAAa,mBACP,YAEF,SAEA,KAAK,KAAK,OAAO,UAAU,UAAU,OAAO,CAAC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { m as Item } from "../Items-D1C2686t.mjs";
|
|
2
|
+
import { i as Turn } from "../Turn-rlTfuHaQ.mjs";
|
|
3
|
+
import { LanguageModel, LanguageModelService } from "../language-model/LanguageModel.mjs";
|
|
4
|
+
import { Duration, Effect, Layer } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/testing/MockProvider.d.ts
|
|
7
|
+
interface MockOptions {
|
|
8
|
+
/**
|
|
9
|
+
* If set, deltas of each scripted turn are spaced by this duration via
|
|
10
|
+
* `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic
|
|
11
|
+
* timing in tests.
|
|
12
|
+
*/
|
|
13
|
+
readonly deltaInterval?: Duration.Input;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A scripted mock provider. Pre-canned `Turn` outputs are returned in order,
|
|
17
|
+
* one per call to `streamTurn`. Each scripted turn is split into synthetic
|
|
18
|
+
* deltas (text → tool_call_start → tool_call_args_delta → ... → turn_complete)
|
|
19
|
+
* so streaming consumers can see realistic delta shapes.
|
|
20
|
+
*/
|
|
21
|
+
interface MockRecorder {
|
|
22
|
+
readonly calls: ReadonlyArray<{
|
|
23
|
+
readonly history: ReadonlyArray<Item>;
|
|
24
|
+
readonly turn: Turn;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
declare const layer: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => Layer.Layer<LanguageModel>;
|
|
28
|
+
/**
|
|
29
|
+
* Synchronous constructor that returns the `LanguageModelService` value
|
|
30
|
+
* directly, plus a recorder. Use this when you want to swap models
|
|
31
|
+
* mid-stream via `Effect.provideService` instead of providing one model
|
|
32
|
+
* for the whole program via `Layer`.
|
|
33
|
+
*/
|
|
34
|
+
declare const make: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => {
|
|
35
|
+
readonly service: LanguageModelService;
|
|
36
|
+
readonly recorder: Effect.Effect<MockRecorder>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Same as `layer`, but also exposes a recorder that captures every call
|
|
40
|
+
* (history + returned turn).
|
|
41
|
+
*/
|
|
42
|
+
declare const layerWithRecorder: (scriptedTurns: ReadonlyArray<Turn>, options?: MockOptions) => {
|
|
43
|
+
readonly layer: Layer.Layer<LanguageModel>;
|
|
44
|
+
readonly recorder: Effect.Effect<MockRecorder>;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { MockOptions, MockRecorder, layer, layerWithRecorder, make };
|
|
48
|
+
//# sourceMappingURL=MockProvider.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MockProvider.d.mts","names":[],"sources":["../../src/testing/MockProvider.ts"],"mappings":";;;;;;UAMiB,WAAA;;AAAjB;;;;WAMW,aAAA,GAAgB,QAAA,CAAS,KAAA;AAAA;;;;AASpC;;;UAAiB,YAAA;EAAA,SACN,KAAA,EAAO,aAAA;IAAA,SACL,OAAA,EAAS,aAAA,CAAc,IAAA;IAAA,SACvB,IAAA,EAAM,IAAA;EAAA;AAAA;AAAA,cAqEN,KAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA,KACT,KAAA,CAAM,KAAA,CAAM,aAAA;;;;;;;cAQF,IAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,OAAA,EAAS,oBAAA;EAAA,SACT,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA;;;;;cAiCtB,iBAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,aAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { InvalidRequest } from "../domain/AiError.mjs";
|
|
2
|
+
import { LanguageModel } from "../language-model/LanguageModel.mjs";
|
|
3
|
+
import { Effect, Layer, Ref, Schedule, Stream } from "effect";
|
|
4
|
+
//#region src/testing/MockProvider.ts
|
|
5
|
+
const turnToDeltas = (turn) => {
|
|
6
|
+
const deltas = [];
|
|
7
|
+
for (const item of turn.items) if (item.type === "message" && item.role === "assistant") {
|
|
8
|
+
for (const block of item.content) if (block.type === "output_text") deltas.push({
|
|
9
|
+
type: "text_delta",
|
|
10
|
+
text: block.text
|
|
11
|
+
});
|
|
12
|
+
} else if (item.type === "function_call") {
|
|
13
|
+
deltas.push({
|
|
14
|
+
type: "tool_call_start",
|
|
15
|
+
call_id: item.call_id,
|
|
16
|
+
name: item.name
|
|
17
|
+
});
|
|
18
|
+
deltas.push({
|
|
19
|
+
type: "tool_call_args_delta",
|
|
20
|
+
call_id: item.call_id,
|
|
21
|
+
delta: item.arguments
|
|
22
|
+
});
|
|
23
|
+
} else if (item.type === "reasoning" && item.summary !== void 0) deltas.push({
|
|
24
|
+
type: "reasoning_delta",
|
|
25
|
+
text: item.summary,
|
|
26
|
+
kind: "summary"
|
|
27
|
+
});
|
|
28
|
+
deltas.push({
|
|
29
|
+
type: "turn_complete",
|
|
30
|
+
turn
|
|
31
|
+
});
|
|
32
|
+
return deltas;
|
|
33
|
+
};
|
|
34
|
+
const pacedDeltas = (turn, options) => {
|
|
35
|
+
const base = Stream.fromIterable(turnToDeltas(turn));
|
|
36
|
+
return options?.deltaInterval === void 0 ? base : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)));
|
|
37
|
+
};
|
|
38
|
+
const makeService = (scriptedTurns, options, recordCall) => Effect.gen(function* () {
|
|
39
|
+
const cursor = yield* Ref.make(0);
|
|
40
|
+
return LanguageModel.of({ streamTurn: (request) => Stream.unwrap(Effect.gen(function* () {
|
|
41
|
+
const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1);
|
|
42
|
+
if (i >= scriptedTurns.length) return Stream.fail(new InvalidRequest({
|
|
43
|
+
provider: "mock",
|
|
44
|
+
raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`
|
|
45
|
+
}));
|
|
46
|
+
const turn = scriptedTurns[i];
|
|
47
|
+
if (recordCall !== void 0) yield* recordCall(request.history, turn);
|
|
48
|
+
return pacedDeltas(turn, options);
|
|
49
|
+
})) });
|
|
50
|
+
});
|
|
51
|
+
const layer = (scriptedTurns, options) => Layer.effect(LanguageModel, makeService(scriptedTurns, options));
|
|
52
|
+
/**
|
|
53
|
+
* Synchronous constructor that returns the `LanguageModelService` value
|
|
54
|
+
* directly, plus a recorder. Use this when you want to swap models
|
|
55
|
+
* mid-stream via `Effect.provideService` instead of providing one model
|
|
56
|
+
* for the whole program via `Layer`.
|
|
57
|
+
*/
|
|
58
|
+
const make = (scriptedTurns, options) => {
|
|
59
|
+
const cursor = Ref.makeUnsafe(0);
|
|
60
|
+
const callsRef = Ref.makeUnsafe([]);
|
|
61
|
+
return {
|
|
62
|
+
service: { streamTurn: (request) => Stream.unwrap(Effect.gen(function* () {
|
|
63
|
+
const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1);
|
|
64
|
+
if (i >= scriptedTurns.length) return Stream.fail(new InvalidRequest({
|
|
65
|
+
provider: "mock",
|
|
66
|
+
raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`
|
|
67
|
+
}));
|
|
68
|
+
const turn = scriptedTurns[i];
|
|
69
|
+
yield* Ref.update(callsRef, (xs) => [...xs, {
|
|
70
|
+
history: request.history,
|
|
71
|
+
turn
|
|
72
|
+
}]);
|
|
73
|
+
return pacedDeltas(turn, options);
|
|
74
|
+
})) },
|
|
75
|
+
recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls })))
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Same as `layer`, but also exposes a recorder that captures every call
|
|
80
|
+
* (history + returned turn).
|
|
81
|
+
*/
|
|
82
|
+
const layerWithRecorder = (scriptedTurns, options) => {
|
|
83
|
+
const callsRef = Ref.makeUnsafe([]);
|
|
84
|
+
return {
|
|
85
|
+
layer: Layer.effect(LanguageModel, makeService(scriptedTurns, options, (history, turn) => Ref.update(callsRef, (xs) => [...xs, {
|
|
86
|
+
history,
|
|
87
|
+
turn
|
|
88
|
+
}]))),
|
|
89
|
+
recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls })))
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
export { layer, layerWithRecorder, make };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=MockProvider.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MockProvider.mjs","names":["AiError.InvalidRequest"],"sources":["../../src/testing/MockProvider.ts"],"sourcesContent":["import { Duration, Effect, Layer, Ref, Schedule, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { Item } from \"../domain/Items.js\"\nimport { LanguageModel, type LanguageModelService } from \"../language-model/LanguageModel.js\"\nimport type { Turn, TurnEvent } from \"../domain/Turn.js\"\n\nexport interface MockOptions {\n /**\n * If set, deltas of each scripted turn are spaced by this duration via\n * `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic\n * timing in tests.\n */\n readonly deltaInterval?: Duration.Input\n}\n\n/**\n * A scripted mock provider. Pre-canned `Turn` outputs are returned in order,\n * one per call to `streamTurn`. Each scripted turn is split into synthetic\n * deltas (text → tool_call_start → tool_call_args_delta → ... → turn_complete)\n * so streaming consumers can see realistic delta shapes.\n */\nexport interface MockRecorder {\n readonly calls: ReadonlyArray<{\n readonly history: ReadonlyArray<Item>\n readonly turn: Turn\n }>\n}\n\nconst turnToDeltas = (turn: Turn): ReadonlyArray<TurnEvent> => {\n const deltas: TurnEvent[] = []\n for (const item of turn.items) {\n if (item.type === \"message\" && item.role === \"assistant\") {\n for (const block of item.content) {\n if (block.type === \"output_text\") {\n deltas.push({ type: \"text_delta\", text: block.text })\n }\n }\n } else if (item.type === \"function_call\") {\n deltas.push({\n type: \"tool_call_start\",\n call_id: item.call_id,\n name: item.name,\n })\n deltas.push({\n type: \"tool_call_args_delta\",\n call_id: item.call_id,\n delta: item.arguments,\n })\n } else if (item.type === \"reasoning\" && item.summary !== undefined) {\n deltas.push({ type: \"reasoning_delta\", text: item.summary, kind: \"summary\" })\n }\n }\n deltas.push({ type: \"turn_complete\", turn })\n return deltas\n}\n\nconst pacedDeltas = (turn: Turn, options?: MockOptions): Stream.Stream<TurnEvent> => {\n const base = Stream.fromIterable(turnToDeltas(turn))\n return options?.deltaInterval === undefined\n ? base\n : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)))\n}\n\nconst makeService = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n recordCall?: (history: ReadonlyArray<Item>, turn: Turn) => Effect.Effect<void>,\n) =>\n Effect.gen(function* () {\n const cursor = yield* Ref.make(0)\n return LanguageModel.of({\n streamTurn: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1)\n if (i >= scriptedTurns.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`,\n }),\n )\n }\n const turn = scriptedTurns[i]!\n if (recordCall !== undefined) {\n yield* recordCall(request.history, turn)\n }\n return pacedDeltas(turn, options)\n }),\n ),\n })\n })\n\nexport const layer = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): Layer.Layer<LanguageModel> => Layer.effect(LanguageModel, makeService(scriptedTurns, options))\n\n/**\n * Synchronous constructor that returns the `LanguageModelService` value\n * directly, plus a recorder. Use this when you want to swap models\n * mid-stream via `Effect.provideService` instead of providing one model\n * for the whole program via `Layer`.\n */\nexport const make = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly service: LanguageModelService\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const cursor = Ref.makeUnsafe(0)\n const callsRef = Ref.makeUnsafe<ReadonlyArray<{ history: ReadonlyArray<Item>; turn: Turn }>>([])\n const service: LanguageModelService = {\n streamTurn: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const i = yield* Ref.getAndUpdate(cursor, (n) => n + 1)\n if (i >= scriptedTurns.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${scriptedTurns.length} turns scripted, but call ${i + 1} was made`,\n }),\n )\n }\n const turn = scriptedTurns[i]!\n yield* Ref.update(callsRef, (xs) => [...xs, { history: request.history, turn }])\n return pacedDeltas(turn, options)\n }),\n ),\n }\n return {\n service,\n recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls }))),\n }\n}\n\n/**\n * Same as `layer`, but also exposes a recorder that captures every call\n * (history + returned turn).\n */\nexport const layerWithRecorder = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly layer: Layer.Layer<LanguageModel>\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const callsRef = Ref.makeUnsafe<ReadonlyArray<{ history: ReadonlyArray<Item>; turn: Turn }>>([])\n const live = Layer.effect(\n LanguageModel,\n makeService(scriptedTurns, options, (history, turn) =>\n Ref.update(callsRef, (xs) => [...xs, { history, turn }]),\n ),\n )\n return {\n layer: live,\n recorder: Ref.get(callsRef).pipe(Effect.map((calls) => ({ calls }))),\n }\n}\n"],"mappings":";;;;AA4BA,MAAM,gBAAgB,SAAyC;CAC7D,MAAM,SAAsB,EAAE;AAC9B,MAAK,MAAM,QAAQ,KAAK,MACtB,KAAI,KAAK,SAAS,aAAa,KAAK,SAAS;OACtC,MAAM,SAAS,KAAK,QACvB,KAAI,MAAM,SAAS,cACjB,QAAO,KAAK;GAAE,MAAM;GAAc,MAAM,MAAM;GAAM,CAAC;YAGhD,KAAK,SAAS,iBAAiB;AACxC,SAAO,KAAK;GACV,MAAM;GACN,SAAS,KAAK;GACd,MAAM,KAAK;GACZ,CAAC;AACF,SAAO,KAAK;GACV,MAAM;GACN,SAAS,KAAK;GACd,OAAO,KAAK;GACb,CAAC;YACO,KAAK,SAAS,eAAe,KAAK,YAAY,KAAA,EACvD,QAAO,KAAK;EAAE,MAAM;EAAmB,MAAM,KAAK;EAAS,MAAM;EAAW,CAAC;AAGjF,QAAO,KAAK;EAAE,MAAM;EAAiB;EAAM,CAAC;AAC5C,QAAO;;AAGT,MAAM,eAAe,MAAY,YAAoD;CACnF,MAAM,OAAO,OAAO,aAAa,aAAa,KAAK,CAAC;AACpD,QAAO,SAAS,kBAAkB,KAAA,IAC9B,OACA,KAAK,KAAK,OAAO,SAAS,SAAS,OAAO,QAAQ,cAAc,CAAC,CAAC;;AAGxE,MAAM,eACJ,eACA,SACA,eAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,IAAI,KAAK,EAAE;AACjC,QAAO,cAAc,GAAG,EACtB,aAAa,YACX,OAAO,OACL,OAAO,IAAI,aAAa;EACtB,MAAM,IAAI,OAAO,IAAI,aAAa,SAAS,MAAM,IAAI,EAAE;AACvD,MAAI,KAAK,cAAc,OACrB,QAAO,OAAO,KACZ,IAAIA,eAAuB;GACzB,UAAU;GACV,KAAK,2BAA2B,cAAc,OAAO,4BAA4B,IAAI,EAAE;GACxF,CAAC,CACH;EAEH,MAAM,OAAO,cAAc;AAC3B,MAAI,eAAe,KAAA,EACjB,QAAO,WAAW,QAAQ,SAAS,KAAK;AAE1C,SAAO,YAAY,MAAM,QAAQ;GACjC,CACH,EACJ,CAAC;EACF;AAEJ,MAAa,SACX,eACA,YAC+B,MAAM,OAAO,eAAe,YAAY,eAAe,QAAQ,CAAC;;;;;;;AAQjG,MAAa,QACX,eACA,YAIG;CACH,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,WAAW,IAAI,WAAwE,EAAE,CAAC;AAoBhG,QAAO;EACL,SAAA,EAnBA,aAAa,YACX,OAAO,OACL,OAAO,IAAI,aAAa;GACtB,MAAM,IAAI,OAAO,IAAI,aAAa,SAAS,MAAM,IAAI,EAAE;AACvD,OAAI,KAAK,cAAc,OACrB,QAAO,OAAO,KACZ,IAAIA,eAAuB;IACzB,UAAU;IACV,KAAK,2BAA2B,cAAc,OAAO,4BAA4B,IAAI,EAAE;IACxF,CAAC,CACH;GAEH,MAAM,OAAO,cAAc;AAC3B,UAAO,IAAI,OAAO,WAAW,OAAO,CAAC,GAAG,IAAI;IAAE,SAAS,QAAQ;IAAS;IAAM,CAAC,CAAC;AAChF,UAAO,YAAY,MAAM,QAAQ;IACjC,CACH,EAGI;EACP,UAAU,IAAI,IAAI,SAAS,CAAC,KAAK,OAAO,KAAK,WAAW,EAAE,OAAO,EAAE,CAAC;EACrE;;;;;;AAOH,MAAa,qBACX,eACA,YAIG;CACH,MAAM,WAAW,IAAI,WAAwE,EAAE,CAAC;AAOhG,QAAO;EACL,OAPW,MAAM,OACjB,eACA,YAAY,eAAe,UAAU,SAAS,SAC5C,IAAI,OAAO,WAAW,OAAO,CAAC,GAAG,IAAI;GAAE;GAAS;GAAM,CAAC,CAAC,CACzD,CAGU;EACX,UAAU,IAAI,IAAI,SAAS,CAAC,KAAK,OAAO,KAAK,WAAW,EAAE,OAAO,EAAE,CAAC;EACrE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { m as Item, o as FunctionCall } from "../Items-D1C2686t.mjs";
|
|
2
|
+
import { n as ToolResult } from "../Outcome-C2JYknCu.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/tool/HistoryCheck.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Return every `function_call` in `history` that does not have a matching
|
|
7
|
+
* `function_call_output` later in `history` (correlated by `call_id`).
|
|
8
|
+
* Empty result = history is provider-submittable from this invariant.
|
|
9
|
+
*/
|
|
10
|
+
declare const findUnansweredCalls: (history: ReadonlyArray<Item>) => ReadonlyArray<FunctionCall>;
|
|
11
|
+
/** Cheap predicate: is this history submittable to a provider? */
|
|
12
|
+
declare const isReconciled: (history: ReadonlyArray<Item>) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Synthesize cancellation results for every unanswered call. Caller maps
|
|
15
|
+
* via `toFunctionCallOutput` and appends to history before submitting.
|
|
16
|
+
*
|
|
17
|
+
* Use when: a new user message arrives mid-approval; an approval timer
|
|
18
|
+
* fires; a persisted checkpoint contains orphans (crash recovery); a
|
|
19
|
+
* stateless HTTP server reconstructed history from a stale checkpoint.
|
|
20
|
+
*/
|
|
21
|
+
declare const cancelAllPending: (history: ReadonlyArray<Item>, reason?: string) => ReadonlyArray<ToolResult>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { cancelAllPending, findUnansweredCalls, isReconciled };
|
|
24
|
+
//# sourceMappingURL=HistoryCheck.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HistoryCheck.d.mts","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"mappings":";;;;;;;;;cAyBa,mBAAA,GACX,OAAA,EAAS,aAAA,CAAc,IAAA,MACtB,aAAA,CAAc,YAAA;AAMjB;AAAA,cAAa,YAAA,GAAgB,OAAA,EAAS,aAAA,CAAc,IAAA;;;;;;;;AAWpD;cAAa,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,IAAA,GACvB,MAAA,cACC,aAAA,CAAc,UAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isFunctionCall, isFunctionCallOutput } from "../domain/Items.mjs";
|
|
2
|
+
import { cancelled } from "./Outcome.mjs";
|
|
3
|
+
//#region src/tool/HistoryCheck.ts
|
|
4
|
+
/**
|
|
5
|
+
* History-consistency primitives. Useful even WITHOUT HITL.
|
|
6
|
+
*
|
|
7
|
+
* Every provider rejects a new request if any prior `function_call` lacks
|
|
8
|
+
* a matching `function_call_output`. Multi-turn flows that can be
|
|
9
|
+
* interrupted, restarted, or branched (HITL, mid-stream abort, persisted
|
|
10
|
+
* checkpoints, stateless HTTP servers) need to detect orphans and
|
|
11
|
+
* synthesize closing outputs before submitting.
|
|
12
|
+
*
|
|
13
|
+
* Recipe author calls these at known transition points (right before the
|
|
14
|
+
* next provider request). Not invoked from inside the loop.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Return every `function_call` in `history` that does not have a matching
|
|
18
|
+
* `function_call_output` later in `history` (correlated by `call_id`).
|
|
19
|
+
* Empty result = history is provider-submittable from this invariant.
|
|
20
|
+
*/
|
|
21
|
+
const findUnansweredCalls = (history) => {
|
|
22
|
+
const answered = new Set(history.filter(isFunctionCallOutput).map((o) => o.call_id));
|
|
23
|
+
return history.filter(isFunctionCall).filter((c) => !answered.has(c.call_id));
|
|
24
|
+
};
|
|
25
|
+
/** Cheap predicate: is this history submittable to a provider? */
|
|
26
|
+
const isReconciled = (history) => findUnansweredCalls(history).length === 0;
|
|
27
|
+
/**
|
|
28
|
+
* Synthesize cancellation results for every unanswered call. Caller maps
|
|
29
|
+
* via `toFunctionCallOutput` and appends to history before submitting.
|
|
30
|
+
*
|
|
31
|
+
* Use when: a new user message arrives mid-approval; an approval timer
|
|
32
|
+
* fires; a persisted checkpoint contains orphans (crash recovery); a
|
|
33
|
+
* stateless HTTP server reconstructed history from a stale checkpoint.
|
|
34
|
+
*/
|
|
35
|
+
const cancelAllPending = (history, reason) => findUnansweredCalls(history).map((call) => cancelled(call, reason));
|
|
36
|
+
//#endregion
|
|
37
|
+
export { cancelAllPending, findUnansweredCalls, isReconciled };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=HistoryCheck.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HistoryCheck.mjs","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"sourcesContent":["/**\n * History-consistency primitives. Useful even WITHOUT HITL.\n *\n * Every provider rejects a new request if any prior `function_call` lacks\n * a matching `function_call_output`. Multi-turn flows that can be\n * interrupted, restarted, or branched (HITL, mid-stream abort, persisted\n * checkpoints, stateless HTTP servers) need to detect orphans and\n * synthesize closing outputs before submitting.\n *\n * Recipe author calls these at known transition points (right before the\n * next provider request). Not invoked from inside the loop.\n */\nimport {\n type FunctionCall,\n type Item,\n isFunctionCall,\n isFunctionCallOutput,\n} from \"../domain/Items.js\"\nimport { type ToolResult, cancelled } from \"./Outcome.js\"\n\n/**\n * Return every `function_call` in `history` that does not have a matching\n * `function_call_output` later in `history` (correlated by `call_id`).\n * Empty result = history is provider-submittable from this invariant.\n */\nexport const findUnansweredCalls = (\n history: ReadonlyArray<Item>,\n): ReadonlyArray<FunctionCall> => {\n const answered = new Set(history.filter(isFunctionCallOutput).map((o) => o.call_id))\n return history.filter(isFunctionCall).filter((c) => !answered.has(c.call_id))\n}\n\n/** Cheap predicate: is this history submittable to a provider? */\nexport const isReconciled = (history: ReadonlyArray<Item>): boolean =>\n findUnansweredCalls(history).length === 0\n\n/**\n * Synthesize cancellation results for every unanswered call. Caller maps\n * via `toFunctionCallOutput` and appends to history before submitting.\n *\n * Use when: a new user message arrives mid-approval; an approval timer\n * fires; a persisted checkpoint contains orphans (crash recovery); a\n * stateless HTTP server reconstructed history from a stale checkpoint.\n */\nexport const cancelAllPending = (\n history: ReadonlyArray<Item>,\n reason?: string,\n): ReadonlyArray<ToolResult> =>\n findUnansweredCalls(history).map((call) => cancelled(call, reason))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,uBACX,YACgC;CAChC,MAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,qBAAqB,CAAC,KAAK,MAAM,EAAE,QAAQ,CAAC;AACpF,QAAO,QAAQ,OAAO,eAAe,CAAC,QAAQ,MAAM,CAAC,SAAS,IAAI,EAAE,QAAQ,CAAC;;;AAI/E,MAAa,gBAAgB,YAC3B,oBAAoB,QAAQ,CAAC,WAAW;;;;;;;;;AAU1C,MAAa,oBACX,SACA,WAEA,oBAAoB,QAAQ,CAAC,KAAK,SAAS,UAAU,MAAM,OAAO,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as execute, c as isValue, d as toFunctionCallOutput, i as denied, l as reject, n as ToolResult, o as executionError, r as cancelled, s as isFailure, t as ToolDecision, u as rejected } from "../Outcome-C2JYknCu.mjs";
|
|
2
|
+
export { ToolDecision, ToolResult, cancelled, denied, execute, executionError, isFailure, isValue, reject, rejected, toFunctionCallOutput };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { functionCallOutput } from "../domain/Items.mjs";
|
|
2
|
+
import { Match } from "effect";
|
|
3
|
+
//#region src/tool/Outcome.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pre-execution decision (`ToolDecision`) and post-execution result
|
|
6
|
+
* (`ToolResult`) for the resolver-based executor.
|
|
7
|
+
*
|
|
8
|
+
* - Resolver returns ToolDecision (Execute | Reject(result)) per call.
|
|
9
|
+
* - Executor emits ToolResult (Value | Failure) per call.
|
|
10
|
+
*
|
|
11
|
+
* Wire conversion stays at the recipe boundary via `toFunctionCallOutput`
|
|
12
|
+
* so recipes can inspect, redact, or audit values before serialization.
|
|
13
|
+
*
|
|
14
|
+
* `output` and `reason` are `string`, not `unknown`: the wire wants strings,
|
|
15
|
+
* and `unknown` would invite non-serializable values (Date, Map, BigInt,
|
|
16
|
+
* fn). Recipes that want structured detail JSON.stringify themselves.
|
|
17
|
+
*/
|
|
18
|
+
const isValue = (r) => r._tag === "Value";
|
|
19
|
+
const isFailure = (r) => r._tag === "Failure";
|
|
20
|
+
const execute = { _tag: "Execute" };
|
|
21
|
+
const reject = (result) => ({
|
|
22
|
+
_tag: "Reject",
|
|
23
|
+
result
|
|
24
|
+
});
|
|
25
|
+
const rejected = (call, kind, reason) => ({
|
|
26
|
+
_tag: "Failure",
|
|
27
|
+
call_id: call.call_id,
|
|
28
|
+
tool: call.name,
|
|
29
|
+
kind,
|
|
30
|
+
...reason !== void 0 ? { reason } : {}
|
|
31
|
+
});
|
|
32
|
+
/** Explicit user/policy rejection. */
|
|
33
|
+
const denied = (call, reason) => rejected(call, "denied", reason);
|
|
34
|
+
/** Implicit non-answer (follow-up, inactivity, abort). */
|
|
35
|
+
const cancelled = (call, reason) => rejected(call, "cancelled", reason);
|
|
36
|
+
/** Tool's own execution failed (parse error, schema, runtime crash). */
|
|
37
|
+
const executionError = (call, reason) => rejected(call, "execution_error", reason);
|
|
38
|
+
const toFunctionCallOutput = (r) => Match.value(r).pipe(Match.tag("Value", (v) => functionCallOutput(v.call_id, JSON.stringify(v.value))), Match.tag("Failure", (f) => functionCallOutput(f.call_id, JSON.stringify(f.reason !== void 0 ? {
|
|
39
|
+
kind: f.kind,
|
|
40
|
+
reason: f.reason
|
|
41
|
+
} : { kind: f.kind }))), Match.exhaustive);
|
|
42
|
+
//#endregion
|
|
43
|
+
export { cancelled, denied, execute, executionError, isFailure, isValue, reject, rejected, toFunctionCallOutput };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=Outcome.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Outcome.mjs","names":[],"sources":["../../src/tool/Outcome.ts"],"sourcesContent":["/**\n * Pre-execution decision (`ToolDecision`) and post-execution result\n * (`ToolResult`) for the resolver-based executor.\n *\n * - Resolver returns ToolDecision (Execute | Reject(result)) per call.\n * - Executor emits ToolResult (Value | Failure) per call.\n *\n * Wire conversion stays at the recipe boundary via `toFunctionCallOutput`\n * so recipes can inspect, redact, or audit values before serialization.\n *\n * `output` and `reason` are `string`, not `unknown`: the wire wants strings,\n * and `unknown` would invite non-serializable values (Date, Map, BigInt,\n * fn). Recipes that want structured detail JSON.stringify themselves.\n */\nimport { Match } from \"effect\"\nimport type { FunctionCall, FunctionCallOutput } from \"../domain/Items.js\"\nimport { functionCallOutput } from \"../domain/Items.js\"\n\n// ---------------------------------------------------------------------------\n// ToolResult\n// ---------------------------------------------------------------------------\n\nexport type ToolResult =\n | {\n readonly _tag: \"Value\"\n readonly call_id: string\n readonly tool: string\n readonly value: unknown\n }\n | {\n readonly _tag: \"Failure\"\n readonly call_id: string\n readonly tool: string\n readonly kind: string\n readonly reason?: string\n }\n\nexport const isValue = (r: ToolResult): r is Extract<ToolResult, { _tag: \"Value\" }> =>\n r._tag === \"Value\"\n\nexport const isFailure = (r: ToolResult): r is Extract<ToolResult, { _tag: \"Failure\" }> =>\n r._tag === \"Failure\"\n\n// ---------------------------------------------------------------------------\n// ToolDecision\n// ---------------------------------------------------------------------------\n\nexport type ToolDecision =\n | { readonly _tag: \"Execute\" }\n | { readonly _tag: \"Reject\"; readonly result: ToolResult }\n\nexport const execute: ToolDecision = { _tag: \"Execute\" }\n\nexport const reject = (result: ToolResult): ToolDecision => ({ _tag: \"Reject\", result })\n\n// ---------------------------------------------------------------------------\n// Synthesizers. `denied` and `cancelled` are operationally distinct;\n// anything else is just a recipe-chosen `kind` via `rejected`.\n// ---------------------------------------------------------------------------\n\nexport const rejected = (\n call: FunctionCall,\n kind: string,\n reason?: string,\n): ToolResult => ({\n _tag: \"Failure\",\n call_id: call.call_id,\n tool: call.name,\n kind,\n ...(reason !== undefined ? { reason } : {}),\n})\n\n/** Explicit user/policy rejection. */\nexport const denied = (call: FunctionCall, reason?: string): ToolResult =>\n rejected(call, \"denied\", reason)\n\n/** Implicit non-answer (follow-up, inactivity, abort). */\nexport const cancelled = (call: FunctionCall, reason?: string): ToolResult =>\n rejected(call, \"cancelled\", reason)\n\n/** Tool's own execution failed (parse error, schema, runtime crash). */\nexport const executionError = (call: FunctionCall, reason: string): ToolResult =>\n rejected(call, \"execution_error\", reason)\n\n// ---------------------------------------------------------------------------\n// Wire conversion - the one place structured → string happens.\n// ---------------------------------------------------------------------------\n\nexport const toFunctionCallOutput = (r: ToolResult): FunctionCallOutput =>\n Match.value(r).pipe(\n Match.tag(\"Value\", (v) => functionCallOutput(v.call_id, JSON.stringify(v.value))),\n Match.tag(\"Failure\", (f) =>\n functionCallOutput(\n f.call_id,\n JSON.stringify(\n f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },\n ),\n ),\n ),\n Match.exhaustive,\n )\n"],"mappings":";;;;;;;;;;;;;;;;;AAqCA,MAAa,WAAW,MACtB,EAAE,SAAS;AAEb,MAAa,aAAa,MACxB,EAAE,SAAS;AAUb,MAAa,UAAwB,EAAE,MAAM,WAAW;AAExD,MAAa,UAAU,YAAsC;CAAE,MAAM;CAAU;CAAQ;AAOvF,MAAa,YACX,MACA,MACA,YACgB;CAChB,MAAM;CACN,SAAS,KAAK;CACd,MAAM,KAAK;CACX;CACA,GAAI,WAAW,KAAA,IAAY,EAAE,QAAQ,GAAG,EAAE;CAC3C;;AAGD,MAAa,UAAU,MAAoB,WACzC,SAAS,MAAM,UAAU,OAAO;;AAGlC,MAAa,aAAa,MAAoB,WAC5C,SAAS,MAAM,aAAa,OAAO;;AAGrC,MAAa,kBAAkB,MAAoB,WACjD,SAAS,MAAM,mBAAmB,OAAO;AAM3C,MAAa,wBAAwB,MACnC,MAAM,MAAM,EAAE,CAAC,KACb,MAAM,IAAI,UAAU,MAAM,mBAAmB,EAAE,SAAS,KAAK,UAAU,EAAE,MAAM,CAAC,CAAC,EACjF,MAAM,IAAI,YAAY,MACpB,mBACE,EAAE,SACF,KAAK,UACH,EAAE,WAAW,KAAA,IAAY;CAAE,MAAM,EAAE;CAAM,QAAQ,EAAE;CAAQ,GAAG,EAAE,MAAM,EAAE,MAAM,CAC/E,CACF,CACF,EACD,MAAM,WACP"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { o as FunctionCall } from "../Items-D1C2686t.mjs";
|
|
2
|
+
import { n as ToolResult, t as ToolDecision } from "../Outcome-C2JYknCu.mjs";
|
|
3
|
+
import { t as ToolEvent } from "../ToolEvent-B2N10hr3.mjs";
|
|
4
|
+
import { Resolver } from "./Toolkit.mjs";
|
|
5
|
+
import { Effect, Queue, Scope, Stream } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/tool/Resolvers.d.ts
|
|
8
|
+
interface Verdict {
|
|
9
|
+
readonly call_id: string;
|
|
10
|
+
readonly decision: "approve" | "deny";
|
|
11
|
+
readonly reason?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Queue-backed resolver. The router fiber drains verdicts and resolves
|
|
15
|
+
* pre-registered Deferreds keyed by `call_id`. Returns the resolver and
|
|
16
|
+
* a stream of `ApprovalRequested` events for the gated calls; the recipe
|
|
17
|
+
* merges the announce stream into its consumer view.
|
|
18
|
+
*/
|
|
19
|
+
declare const fromVerdictQueue: (predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) => (calls: ReadonlyArray<FunctionCall>) => Effect.Effect<{
|
|
20
|
+
readonly resolve: Resolver;
|
|
21
|
+
readonly announce: Stream.Stream<ToolEvent>;
|
|
22
|
+
}, never, Scope.Scope>;
|
|
23
|
+
type ApprovalMapEntry = {
|
|
24
|
+
readonly decision: "approve";
|
|
25
|
+
} | {
|
|
26
|
+
readonly decision: "deny";
|
|
27
|
+
readonly reason?: string;
|
|
28
|
+
};
|
|
29
|
+
declare const fromApprovalMap: (predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) => Resolver;
|
|
30
|
+
/**
|
|
31
|
+
* Authz gate. `canApprove` runs BEFORE the inner resolver; failures
|
|
32
|
+
* short-circuit to a `permission_denied` rejection. Override `onForbidden`
|
|
33
|
+
* if your audit format wants a different kind or reason.
|
|
34
|
+
*/
|
|
35
|
+
declare const withPermissions: (inner: Resolver, canApprove: (call: FunctionCall) => Effect.Effect<boolean>, onForbidden?: (call: FunctionCall) => ToolResult) => Resolver;
|
|
36
|
+
/**
|
|
37
|
+
* Fallback gate. If `inner` returns a Reject whose result matches the
|
|
38
|
+
* `recoverable` predicate, run `fallback(call)` instead and use that
|
|
39
|
+
* decision. Otherwise pass the original Reject through.
|
|
40
|
+
*/
|
|
41
|
+
declare const withFallback: (inner: Resolver, recoverable: (result: ToolResult) => boolean, fallback: (call: FunctionCall) => Effect.Effect<ToolDecision>) => Resolver;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ApprovalMapEntry, Verdict, fromApprovalMap, fromVerdictQueue, withFallback, withPermissions };
|
|
44
|
+
//# sourceMappingURL=Resolvers.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Resolvers.d.mts","names":[],"sources":["../../src/tool/Resolvers.ts"],"mappings":";;;;;;;UA8BiB,OAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;;;;;;;cASE,gBAAA,GAET,SAAA,GAAY,IAAA,EAAM,YAAA,cAClB,QAAA,EAAU,KAAA,CAAM,OAAA,CAAQ,OAAA,OAGxB,KAAA,EAAO,aAAA,CAAc,YAAA,MACpB,MAAA,CAAO,MAAA;EAAA,SAEG,OAAA,EAAS,QAAA;EAAA,SACT,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,UAGnC,KAAA,CAAM,KAAA;AAAA,KAkDE,gBAAA;EAAA,SACG,QAAA;AAAA;EAAA,SACA,QAAA;EAAA,SAA2B,MAAA;AAAA;AAAA,cAE7B,eAAA,GAET,SAAA,GAAY,IAAA,EAAM,YAAA,cAClB,SAAA,EAAW,WAAA,SAAoB,gBAAA,MAC9B,QAAA;;;;;;cAmBQ,eAAA,GAET,KAAA,EAAO,QAAA,EACP,UAAA,GAAa,IAAA,EAAM,YAAA,KAAiB,MAAA,CAAO,MAAA,WAC3C,WAAA,IAAc,IAAA,EAAM,YAAA,KAAiB,UAAA,KAEpC,QAAA;;;;;;cAaQ,YAAA,GAET,KAAA,EAAO,QAAA,EACP,WAAA,GAAc,MAAA,EAAQ,UAAA,cACtB,QAAA,GAAW,IAAA,EAAM,YAAA,KAAiB,MAAA,CAAO,MAAA,CAAO,YAAA,MAC/C,QAAA"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cancelled, denied, execute, reject, rejected } from "./Outcome.mjs";
|
|
2
|
+
import { Deferred, Effect, Queue, Stream } from "effect";
|
|
3
|
+
//#region src/tool/Resolvers.ts
|
|
4
|
+
/**
|
|
5
|
+
* Ready-made `Resolver`s for the two transport flavors plus combinators
|
|
6
|
+
* for layering policy on top.
|
|
7
|
+
*
|
|
8
|
+
* - `fromVerdictQueue` : long-lived channel (WebSocket / SSE).
|
|
9
|
+
* - `fromApprovalMap` : request-shaped (HTTP chat).
|
|
10
|
+
* - `withPermissions` : authz wrapper.
|
|
11
|
+
* - `withFallback` : recovery wrapper.
|
|
12
|
+
*
|
|
13
|
+
* None of these know about the executor's stream shape; they just produce
|
|
14
|
+
* `Effect<ToolDecision>`s a `Resolver` can return.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Queue-backed resolver. The router fiber drains verdicts and resolves
|
|
18
|
+
* pre-registered Deferreds keyed by `call_id`. Returns the resolver and
|
|
19
|
+
* a stream of `ApprovalRequested` events for the gated calls; the recipe
|
|
20
|
+
* merges the announce stream into its consumer view.
|
|
21
|
+
*/
|
|
22
|
+
const fromVerdictQueue = (predicate, verdicts) => (calls) => Effect.gen(function* () {
|
|
23
|
+
const gated = calls.filter(predicate);
|
|
24
|
+
const entries = yield* Effect.forEach(gated, (call) => Deferred.make().pipe(Effect.map((d) => [call.call_id, d])));
|
|
25
|
+
const deferreds = new Map(entries);
|
|
26
|
+
yield* Effect.forkScoped(Effect.forever(Effect.gen(function* () {
|
|
27
|
+
const v = yield* Queue.take(verdicts);
|
|
28
|
+
const d = deferreds.get(v.call_id);
|
|
29
|
+
if (d !== void 0) yield* Deferred.succeed(d, v);
|
|
30
|
+
})));
|
|
31
|
+
const resolve = (call) => {
|
|
32
|
+
if (!predicate(call)) return Effect.succeed(execute);
|
|
33
|
+
const d = deferreds.get(call.call_id);
|
|
34
|
+
return Deferred.await(d).pipe(Effect.map((v) => v.decision === "approve" ? execute : reject(denied(call, v.reason))));
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
resolve,
|
|
38
|
+
announce: Stream.fromIterable(gated.map((call) => ({
|
|
39
|
+
_tag: "ApprovalRequested",
|
|
40
|
+
call_id: call.call_id,
|
|
41
|
+
tool: call.name,
|
|
42
|
+
arguments: call.arguments
|
|
43
|
+
})))
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const fromApprovalMap = (predicate, approvals) => (call) => {
|
|
47
|
+
if (!predicate(call)) return Effect.succeed(execute);
|
|
48
|
+
const v = approvals.get(call.call_id);
|
|
49
|
+
if (v === void 0) return Effect.succeed(reject(cancelled(call)));
|
|
50
|
+
return Effect.succeed(v.decision === "approve" ? execute : reject(denied(call, v.reason)));
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Authz gate. `canApprove` runs BEFORE the inner resolver; failures
|
|
54
|
+
* short-circuit to a `permission_denied` rejection. Override `onForbidden`
|
|
55
|
+
* if your audit format wants a different kind or reason.
|
|
56
|
+
*/
|
|
57
|
+
const withPermissions = (inner, canApprove, onForbidden = (call) => rejected(call, "permission_denied", "missing permissions")) => (call) => canApprove(call).pipe(Effect.flatMap((allowed) => allowed ? inner(call) : Effect.succeed(reject(onForbidden(call)))));
|
|
58
|
+
/**
|
|
59
|
+
* Fallback gate. If `inner` returns a Reject whose result matches the
|
|
60
|
+
* `recoverable` predicate, run `fallback(call)` instead and use that
|
|
61
|
+
* decision. Otherwise pass the original Reject through.
|
|
62
|
+
*/
|
|
63
|
+
const withFallback = (inner, recoverable, fallback) => (call) => inner(call).pipe(Effect.flatMap((decision) => decision._tag === "Reject" && recoverable(decision.result) ? fallback(call) : Effect.succeed(decision)));
|
|
64
|
+
//#endregion
|
|
65
|
+
export { fromApprovalMap, fromVerdictQueue, withFallback, withPermissions };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=Resolvers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Resolvers.mjs","names":[],"sources":["../../src/tool/Resolvers.ts"],"sourcesContent":["/**\n * Ready-made `Resolver`s for the two transport flavors plus combinators\n * for layering policy on top.\n *\n * - `fromVerdictQueue` : long-lived channel (WebSocket / SSE).\n * - `fromApprovalMap` : request-shaped (HTTP chat).\n * - `withPermissions` : authz wrapper.\n * - `withFallback` : recovery wrapper.\n *\n * None of these know about the executor's stream shape; they just produce\n * `Effect<ToolDecision>`s a `Resolver` can return.\n */\nimport { Deferred, Effect, Queue, Scope, Stream } from \"effect\"\nimport type { FunctionCall } from \"../domain/Items.js\"\nimport {\n type ToolDecision,\n type ToolResult,\n cancelled,\n denied,\n execute,\n reject,\n rejected,\n} from \"./Outcome.js\"\nimport type { Resolver } from \"./Toolkit.js\"\nimport type { ToolEvent } from \"./ToolEvent.js\"\n\n// ---------------------------------------------------------------------------\n// Verdict queue (WebSocket-style transport).\n// ---------------------------------------------------------------------------\n\nexport interface Verdict {\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n}\n\n/**\n * Queue-backed resolver. The router fiber drains verdicts and resolves\n * pre-registered Deferreds keyed by `call_id`. Returns the resolver and\n * a stream of `ApprovalRequested` events for the gated calls; the recipe\n * merges the announce stream into its consumer view.\n */\nexport const fromVerdictQueue =\n (\n predicate: (call: FunctionCall) => boolean,\n verdicts: Queue.Dequeue<Verdict>,\n ) =>\n (\n calls: ReadonlyArray<FunctionCall>,\n ): Effect.Effect<\n {\n readonly resolve: Resolver\n readonly announce: Stream.Stream<ToolEvent>\n },\n never,\n Scope.Scope\n > =>\n Effect.gen(function* () {\n const gated = calls.filter(predicate)\n\n const entries = yield* Effect.forEach(gated, (call) =>\n Deferred.make<Verdict>().pipe(Effect.map((d) => [call.call_id, d] as const)),\n )\n const deferreds: ReadonlyMap<string, Deferred.Deferred<Verdict>> = new Map(entries)\n\n // Router is forked into the surrounding Scope so it lives as long\n // as the consumer is pulling events. Recipes typically supply the\n // scope by wrapping the events construction in `Stream.unwrap`.\n yield* Effect.forkScoped(\n Effect.forever(\n Effect.gen(function* () {\n const v = yield* Queue.take(verdicts)\n const d = deferreds.get(v.call_id)\n if (d !== undefined) yield* Deferred.succeed(d, v)\n }),\n ),\n )\n\n const resolve: Resolver = (call) => {\n if (!predicate(call)) return Effect.succeed(execute)\n const d = deferreds.get(call.call_id)!\n return Deferred.await(d).pipe(\n Effect.map((v) =>\n v.decision === \"approve\" ? execute : reject(denied(call, v.reason)),\n ),\n )\n }\n\n const announce = Stream.fromIterable<ToolEvent>(\n gated.map((call) => ({\n _tag: \"ApprovalRequested\",\n call_id: call.call_id,\n tool: call.name,\n arguments: call.arguments,\n })),\n )\n\n return { resolve, announce }\n })\n\n// ---------------------------------------------------------------------------\n// Approval map (HTTP-style transport). Verdicts arrive synchronously\n// bundled in the request payload. Missing entries → cancelled.\n// ---------------------------------------------------------------------------\n\nexport type ApprovalMapEntry =\n | { readonly decision: \"approve\" }\n | { readonly decision: \"deny\"; readonly reason?: string }\n\nexport const fromApprovalMap =\n (\n predicate: (call: FunctionCall) => boolean,\n approvals: ReadonlyMap<string, ApprovalMapEntry>,\n ): Resolver =>\n (call) => {\n if (!predicate(call)) return Effect.succeed(execute)\n const v = approvals.get(call.call_id)\n if (v === undefined) return Effect.succeed(reject(cancelled(call)))\n return Effect.succeed(\n v.decision === \"approve\" ? execute : reject(denied(call, v.reason)),\n )\n }\n\n// ---------------------------------------------------------------------------\n// Combinators - compose policy onto an inner resolver.\n// ---------------------------------------------------------------------------\n\n/**\n * Authz gate. `canApprove` runs BEFORE the inner resolver; failures\n * short-circuit to a `permission_denied` rejection. Override `onForbidden`\n * if your audit format wants a different kind or reason.\n */\nexport const withPermissions =\n (\n inner: Resolver,\n canApprove: (call: FunctionCall) => Effect.Effect<boolean>,\n onForbidden: (call: FunctionCall) => ToolResult = (call) =>\n rejected(call, \"permission_denied\", \"missing permissions\"),\n ): Resolver =>\n (call) =>\n canApprove(call).pipe(\n Effect.flatMap((allowed) =>\n allowed ? inner(call) : Effect.succeed(reject(onForbidden(call))),\n ),\n )\n\n/**\n * Fallback gate. If `inner` returns a Reject whose result matches the\n * `recoverable` predicate, run `fallback(call)` instead and use that\n * decision. Otherwise pass the original Reject through.\n */\nexport const withFallback =\n (\n inner: Resolver,\n recoverable: (result: ToolResult) => boolean,\n fallback: (call: FunctionCall) => Effect.Effect<ToolDecision>,\n ): Resolver =>\n (call) =>\n inner(call).pipe(\n Effect.flatMap((decision) =>\n decision._tag === \"Reject\" && recoverable(decision.result)\n ? fallback(call)\n : Effect.succeed(decision),\n ),\n )\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,oBAET,WACA,cAGA,UASA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,MAAM,OAAO,UAAU;CAErC,MAAM,UAAU,OAAO,OAAO,QAAQ,QAAQ,SAC5C,SAAS,MAAe,CAAC,KAAK,OAAO,KAAK,MAAM,CAAC,KAAK,SAAS,EAAE,CAAU,CAAC,CAC7E;CACD,MAAM,YAA6D,IAAI,IAAI,QAAQ;AAKnF,QAAO,OAAO,WACZ,OAAO,QACL,OAAO,IAAI,aAAa;EACtB,MAAM,IAAI,OAAO,MAAM,KAAK,SAAS;EACrC,MAAM,IAAI,UAAU,IAAI,EAAE,QAAQ;AAClC,MAAI,MAAM,KAAA,EAAW,QAAO,SAAS,QAAQ,GAAG,EAAE;GAClD,CACH,CACF;CAED,MAAM,WAAqB,SAAS;AAClC,MAAI,CAAC,UAAU,KAAK,CAAE,QAAO,OAAO,QAAQ,QAAQ;EACpD,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,SAAO,SAAS,MAAM,EAAE,CAAC,KACvB,OAAO,KAAK,MACV,EAAE,aAAa,YAAY,UAAU,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC,CACpE,CACF;;AAYH,QAAO;EAAE;EAAS,UATD,OAAO,aACtB,MAAM,KAAK,UAAU;GACnB,MAAM;GACN,SAAS,KAAK;GACd,MAAM,KAAK;GACX,WAAW,KAAK;GACjB,EAAE,CAGqB;EAAE;EAC5B;AAWN,MAAa,mBAET,WACA,eAED,SAAS;AACR,KAAI,CAAC,UAAU,KAAK,CAAE,QAAO,OAAO,QAAQ,QAAQ;CACpD,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,KAAI,MAAM,KAAA,EAAW,QAAO,OAAO,QAAQ,OAAO,UAAU,KAAK,CAAC,CAAC;AACnE,QAAO,OAAO,QACZ,EAAE,aAAa,YAAY,UAAU,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC,CACpE;;;;;;;AAYL,MAAa,mBAET,OACA,YACA,eAAmD,SACjD,SAAS,MAAM,qBAAqB,sBAAsB,MAE7D,SACC,WAAW,KAAK,CAAC,KACf,OAAO,SAAS,YACd,UAAU,MAAM,KAAK,GAAG,OAAO,QAAQ,OAAO,YAAY,KAAK,CAAC,CAAC,CAClE,CACF;;;;;;AAOL,MAAa,gBAET,OACA,aACA,cAED,SACC,MAAM,KAAK,CAAC,KACV,OAAO,SAAS,aACd,SAAS,SAAS,YAAY,YAAY,SAAS,OAAO,GACtD,SAAS,KAAK,GACd,OAAO,QAAQ,SAAS,CAC7B,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Tool, c as ToolInputSchema, d as fromEffectSchema, f as isStreamingTool, h as toDescriptors, i as StreamingTool, m as streaming, n as AnyPlainTool, o as ToolDescriptor, p as make, r as AnyStreamingTool, s as ToolError, t as AnyKindTool, u as execute } from "../Tool-5wxOCuOh.mjs";
|
|
2
|
+
export { AnyKindTool, AnyPlainTool, AnyStreamingTool, StreamingTool, Tool, ToolDescriptor, ToolError, ToolInputSchema, execute, fromEffectSchema, isStreamingTool, make, streaming, toDescriptors };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { functionCallOutput } from "../domain/Items.mjs";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
//#region src/tool/Tool.ts
|
|
5
|
+
var Tool_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
ToolError: () => ToolError,
|
|
7
|
+
execute: () => execute,
|
|
8
|
+
fromEffectSchema: () => fromEffectSchema,
|
|
9
|
+
isStreamingTool: () => isStreamingTool,
|
|
10
|
+
make: () => make,
|
|
11
|
+
streaming: () => streaming,
|
|
12
|
+
toDescriptors: () => toDescriptors
|
|
13
|
+
});
|
|
14
|
+
var ToolError = class extends Schema.TaggedErrorClass("@betalyra/effect-uai/ToolError")("ToolError", {
|
|
15
|
+
call_id: Schema.String,
|
|
16
|
+
tool: Schema.String,
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
cause: Schema.optional(Schema.Unknown)
|
|
19
|
+
}) {};
|
|
20
|
+
/**
|
|
21
|
+
* Convenience wrapper for Effect Schema users - adds both the
|
|
22
|
+
* `validate` and `jsonSchema` extensions to a plain Effect Schema so it
|
|
23
|
+
* can be used as a `Tool.inputSchema`.
|
|
24
|
+
*/
|
|
25
|
+
const fromEffectSchema = (schema) => Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema));
|
|
26
|
+
const make = (spec) => spec;
|
|
27
|
+
const streaming = (spec) => ({
|
|
28
|
+
_kind: "streaming",
|
|
29
|
+
...spec
|
|
30
|
+
});
|
|
31
|
+
const isStreamingTool = (t) => "_kind" in t && t._kind === "streaming";
|
|
32
|
+
/**
|
|
33
|
+
* Render any-kind tools (mixed plain and streaming) to provider-agnostic
|
|
34
|
+
* descriptors. Mirrors `Toolkit.toDescriptors` but accepts the union type
|
|
35
|
+
* so a single list can carry both kinds.
|
|
36
|
+
*/
|
|
37
|
+
const toDescriptors = (tools) => tools.map((tool) => {
|
|
38
|
+
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({ target: "draft-2020-12" });
|
|
39
|
+
return tool.strict !== void 0 ? {
|
|
40
|
+
name: tool.name,
|
|
41
|
+
description: tool.description,
|
|
42
|
+
inputSchema,
|
|
43
|
+
strict: tool.strict
|
|
44
|
+
} : {
|
|
45
|
+
name: tool.name,
|
|
46
|
+
description: tool.description,
|
|
47
|
+
inputSchema
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const toToolError = (call, toolName, message) => (cause) => new ToolError({
|
|
51
|
+
call_id: call.call_id,
|
|
52
|
+
tool: toolName,
|
|
53
|
+
message,
|
|
54
|
+
cause
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Decode and validate the JSON arguments of a function_call against the
|
|
58
|
+
* tool's input schema, run the tool, and serialize the output into a
|
|
59
|
+
* function_call_output item.
|
|
60
|
+
*/
|
|
61
|
+
const execute = (tool, call) => Effect.gen(function* () {
|
|
62
|
+
const parsed = yield* Effect.try({
|
|
63
|
+
try: () => JSON.parse(call.arguments),
|
|
64
|
+
catch: toToolError(call, tool.name, "Failed to parse JSON arguments")
|
|
65
|
+
});
|
|
66
|
+
const result = yield* Effect.promise(() => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)));
|
|
67
|
+
if (result.issues !== void 0) return yield* new ToolError({
|
|
68
|
+
call_id: call.call_id,
|
|
69
|
+
tool: tool.name,
|
|
70
|
+
message: "Tool input failed schema validation",
|
|
71
|
+
cause: result.issues
|
|
72
|
+
});
|
|
73
|
+
const output = yield* tool.run(result.value).pipe(Effect.mapError(toToolError(call, tool.name, "Tool execution failed")));
|
|
74
|
+
return functionCallOutput(call.call_id, JSON.stringify(output));
|
|
75
|
+
});
|
|
76
|
+
//#endregion
|
|
77
|
+
export { ToolError, execute, fromEffectSchema, isStreamingTool, make, streaming, Tool_exports as t, toDescriptors };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=Tool.mjs.map
|