@effect-uai/core 0.5.1 → 0.6.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/{Items-BH8xUkoR.d.mts → Items-pXR5BAgW.d.mts} +12 -12
- package/dist/Items-pXR5BAgW.d.mts.map +1 -0
- package/dist/SandboxError-BvKypkJm.d.mts +158 -0
- package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
- package/dist/{Tool-87ViKCCO.d.mts → Tool-DP-AwKOf.d.mts} +11 -11
- package/dist/{Tool-87ViKCCO.d.mts.map → Tool-DP-AwKOf.d.mts.map} +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +15 -15
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Turn.d.mts +388 -2
- package/dist/domain/Turn.d.mts.map +1 -0
- package/dist/domain/Turn.mjs +13 -13
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/index.d.mts +12 -6
- package/dist/index.mjs +9 -3
- package/dist/language-model/LanguageModel.d.mts +29 -27
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +16 -25
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +10 -81
- package/dist/language-model/LanguageModel.test.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +37 -90
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -93
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +46 -54
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/retry/Retry.d.mts +64 -0
- package/dist/retry/Retry.d.mts.map +1 -0
- package/dist/retry/Retry.mjs +69 -0
- package/dist/retry/Retry.mjs.map +1 -0
- package/dist/retry/Retry.test.mjs +142 -0
- package/dist/retry/Retry.test.mjs.map +1 -0
- package/dist/sandbox/Memory.d.mts +53 -0
- package/dist/sandbox/Memory.d.mts.map +1 -0
- package/dist/sandbox/Memory.mjs +68 -0
- package/dist/sandbox/Memory.mjs.map +1 -0
- package/dist/sandbox/Sandbox.d.mts +717 -0
- package/dist/sandbox/Sandbox.d.mts.map +1 -0
- package/dist/sandbox/Sandbox.mjs +192 -0
- package/dist/sandbox/Sandbox.mjs.map +1 -0
- package/dist/sandbox/SandboxError.d.mts +2 -0
- package/dist/sandbox/SandboxError.mjs +89 -0
- package/dist/sandbox/SandboxError.mjs.map +1 -0
- package/dist/sandbox/SandboxImage.d.mts +28 -0
- package/dist/sandbox/SandboxImage.d.mts.map +1 -0
- package/dist/sandbox/SandboxImage.mjs +32 -0
- package/dist/sandbox/SandboxImage.mjs.map +1 -0
- package/dist/sandbox/SandboxNetwork.d.mts +34 -0
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
- package/dist/sandbox/SandboxNetwork.mjs +36 -0
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +98 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +56 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +3 -3
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs +14 -8
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +604 -0
- package/dist/testing/MockSandbox.d.mts.map +1 -0
- package/dist/testing/MockSandbox.mjs +160 -0
- package/dist/testing/MockSandbox.mjs.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts +26 -11
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
- package/dist/tool/Approval.d.mts.map +1 -0
- package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
- package/dist/tool/Approval.mjs.map +1 -0
- package/dist/tool/Approval.test.d.mts +1 -0
- package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
- package/dist/tool/Approval.test.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -6
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +5 -5
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +19 -19
- package/dist/tool/ToolEvent.d.mts.map +1 -1
- package/dist/tool/ToolEvent.mjs +8 -8
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
- package/dist/tool/ToolResult.d.mts.map +1 -0
- package/dist/tool/ToolResult.mjs +61 -0
- package/dist/tool/ToolResult.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +58 -26
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +62 -34
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +13 -13
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/package.json +35 -7
- package/src/domain/Items.ts +9 -9
- package/src/domain/Turn.ts +16 -16
- package/src/index.ts +8 -2
- package/src/language-model/LanguageModel.test.ts +21 -102
- package/src/language-model/LanguageModel.ts +36 -59
- package/src/loop/Loop.test.ts +93 -91
- package/src/loop/Loop.ts +72 -170
- package/src/retry/Retry.test.ts +163 -0
- package/src/retry/Retry.ts +84 -0
- package/src/sandbox/Memory.ts +128 -0
- package/src/sandbox/Sandbox.ts +639 -0
- package/src/sandbox/SandboxError.ts +140 -0
- package/src/sandbox/SandboxImage.ts +26 -0
- package/src/sandbox/SandboxNetwork.ts +34 -0
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +89 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +124 -0
- package/src/testing/MockProvider.ts +31 -25
- package/src/testing/MockSandbox.ts +342 -0
- package/src/testing/MockSpeechSynthesizer.ts +137 -77
- package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
- package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
- package/src/tool/HistoryCheck.ts +10 -13
- package/src/tool/Tool.ts +13 -15
- package/src/tool/ToolEvent.ts +7 -7
- package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
- package/src/tool/Toolkit.test.ts +13 -13
- package/src/tool/Toolkit.ts +97 -73
- package/dist/Items-BH8xUkoR.d.mts.map +0 -1
- package/dist/Turn-0CwCAyVe.d.mts +0 -388
- package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
- package/dist/tool/Outcome.d.mts.map +0 -1
- package/dist/tool/Outcome.mjs +0 -61
- package/dist/tool/Outcome.mjs.map +0 -1
- package/dist/tool/Resolvers.d.mts.map +0 -1
- package/dist/tool/Resolvers.mjs.map +0 -1
- package/dist/tool/Resolvers.test.mjs.map +0 -1
- /package/dist/{tool/Resolvers.test.d.mts → retry/Retry.test.d.mts} +0 -0
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
|
-
import { cancelled, denied } from "./
|
|
2
|
+
import { cancelled, denied } from "./ToolResult.mjs";
|
|
3
3
|
import { ToolEvent } from "./ToolEvent.mjs";
|
|
4
4
|
import { Data, Deferred, Effect, Queue, Stream } from "effect";
|
|
5
|
-
//#region src/tool/
|
|
5
|
+
//#region src/tool/Approval.ts
|
|
6
6
|
/**
|
|
7
7
|
* Approval helpers for the two transport flavors.
|
|
8
8
|
*
|
|
9
9
|
* These helpers only decide which calls are approved and which synthetic
|
|
10
10
|
* results must be returned to the model. Tool execution stays explicit at
|
|
11
|
-
* the recipe boundary via `Toolkit.
|
|
11
|
+
* the recipe boundary via `Toolkit.run`.
|
|
12
12
|
*/
|
|
13
|
-
var
|
|
14
|
-
|
|
13
|
+
var Approval_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
ApprovalDecision: () => ApprovalDecision,
|
|
15
15
|
approvalRequested: () => approvalRequested,
|
|
16
16
|
approve: () => approve,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
fromMap: () => fromMap,
|
|
18
|
+
fromQueue: () => fromQueue,
|
|
19
19
|
reject: () => reject,
|
|
20
|
-
|
|
20
|
+
splitApprovalDecisions: () => splitApprovalDecisions
|
|
21
21
|
});
|
|
22
|
-
const
|
|
23
|
-
const approve = (call) =>
|
|
24
|
-
const reject = (result) =>
|
|
25
|
-
const
|
|
22
|
+
const ApprovalDecision = Data.taggedEnum();
|
|
23
|
+
const approve = (call) => ApprovalDecision.Approved({ call });
|
|
24
|
+
const reject = (result) => ApprovalDecision.Rejected({ result });
|
|
25
|
+
const splitApprovalDecisions = (decisions) => decisions.reduce((acc, decision) => decision._tag === "Approved" ? {
|
|
26
26
|
...acc,
|
|
27
27
|
approved: [...acc.approved, decision.call]
|
|
28
28
|
} : {
|
|
@@ -40,9 +40,9 @@ const approvalRequested = (call) => ToolEvent.ApprovalRequested({
|
|
|
40
40
|
/**
|
|
41
41
|
* Queue-backed approval planner. Safe calls are returned immediately in
|
|
42
42
|
* `approved`; gated calls emit `ApprovalRequested` events and later produce
|
|
43
|
-
* one `
|
|
43
|
+
* one `ApprovalDecision` when their matching verdict arrives.
|
|
44
44
|
*/
|
|
45
|
-
const
|
|
45
|
+
const fromQueue = (predicate, verdicts) => (calls) => Effect.gen(function* () {
|
|
46
46
|
const gated = calls.filter(predicate);
|
|
47
47
|
const approved = calls.filter((call) => !predicate(call));
|
|
48
48
|
const entries = yield* Effect.forEach(gated, (call) => Deferred.make().pipe(Effect.map((d) => [call.call_id, d])));
|
|
@@ -58,16 +58,16 @@ const fromVerdictQueue = (predicate, verdicts) => (calls) => Effect.gen(function
|
|
|
58
58
|
const d = deferreds.get(call.call_id);
|
|
59
59
|
return Stream.fromEffect(Deferred.await(d).pipe(Effect.map((v) => v.decision === "approve" ? approve(call) : reject(denied(call, v.reason)))));
|
|
60
60
|
}, { concurrency: "unbounded" })),
|
|
61
|
-
|
|
61
|
+
approvalRequests: Stream.fromIterable(gated.map(approvalRequested))
|
|
62
62
|
};
|
|
63
63
|
});
|
|
64
|
-
const
|
|
64
|
+
const fromMap = (predicate, approvals) => (calls) => splitApprovalDecisions(calls.map((call) => {
|
|
65
65
|
if (!predicate(call)) return approve(call);
|
|
66
66
|
const v = approvals.get(call.call_id);
|
|
67
67
|
if (v === void 0) return reject(cancelled(call));
|
|
68
68
|
return v.decision === "approve" ? approve(call) : reject(denied(call, v.reason));
|
|
69
69
|
}));
|
|
70
70
|
//#endregion
|
|
71
|
-
export {
|
|
71
|
+
export { ApprovalDecision, approvalRequested, approve, fromMap, fromQueue, reject, splitApprovalDecisions, Approval_exports as t };
|
|
72
72
|
|
|
73
|
-
//# sourceMappingURL=
|
|
73
|
+
//# sourceMappingURL=Approval.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Approval.mjs","names":[],"sources":["../../src/tool/Approval.ts"],"sourcesContent":["/**\n * Approval helpers for the two transport flavors.\n *\n * These helpers only decide which calls are approved and which synthetic\n * results must be returned to the model. Tool execution stays explicit at\n * the recipe boundary via `Toolkit.run`.\n */\nimport { Data, Deferred, Effect, Queue, Scope, Stream } from \"effect\"\nimport type { ToolCall } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled, denied } from \"./ToolResult.js\"\nimport { ToolEvent } from \"./ToolEvent.js\"\n\nexport type ToolCallPlan = {\n readonly approved: ReadonlyArray<ToolCall>\n readonly rejected: ReadonlyArray<ToolResult>\n}\n\nexport type ApprovalDecision = Data.TaggedEnum<{\n Approved: { readonly call: ToolCall }\n Rejected: { readonly result: ToolResult }\n}>\n\nexport const ApprovalDecision = Data.taggedEnum<ApprovalDecision>()\n\nexport const approve = (call: ToolCall): ApprovalDecision => ApprovalDecision.Approved({ call })\n\nexport const reject = (result: ToolResult): ApprovalDecision =>\n ApprovalDecision.Rejected({ result })\n\nexport const splitApprovalDecisions = (decisions: ReadonlyArray<ApprovalDecision>): ToolCallPlan =>\n decisions.reduce<ToolCallPlan>(\n (acc, decision) =>\n decision._tag === \"Approved\"\n ? { ...acc, approved: [...acc.approved, decision.call] }\n : { ...acc, rejected: [...acc.rejected, decision.result] },\n { approved: [], rejected: [] },\n )\n\nexport const approvalRequested = (call: ToolCall): ToolEvent =>\n ToolEvent.ApprovalRequested({\n call_id: call.call_id,\n tool: call.name,\n arguments: call.arguments,\n })\n\n// ---------------------------------------------------------------------------\n// Verdict queue (WebSocket-style transport).\n// ---------------------------------------------------------------------------\n\nexport type Verdict = {\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n}\n\n/**\n * Queue-backed approval planner. Safe calls are returned immediately in\n * `approved`; gated calls emit `ApprovalRequested` events and later produce\n * one `ApprovalDecision` when their matching verdict arrives.\n */\nexport const fromQueue =\n (predicate: (call: ToolCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>\n (\n calls: ReadonlyArray<ToolCall>,\n ): Effect.Effect<\n {\n readonly approved: ReadonlyArray<ToolCall>\n readonly decisions: Stream.Stream<ApprovalDecision>\n readonly approvalRequests: Stream.Stream<ToolEvent>\n },\n never,\n Scope.Scope\n > =>\n Effect.gen(function* () {\n const gated = calls.filter(predicate)\n const approved = calls.filter((call) => !predicate(call))\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 decisions = Stream.fromIterable(gated).pipe(\n Stream.flatMap(\n (call) => {\n const d = deferreds.get(call.call_id)!\n return Stream.fromEffect(\n Deferred.await(d).pipe(\n Effect.map((v) =>\n v.decision === \"approve\" ? approve(call) : reject(denied(call, v.reason)),\n ),\n ),\n )\n },\n { concurrency: \"unbounded\" },\n ),\n )\n\n const approvalRequests = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))\n\n return { approved, decisions, approvalRequests }\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 fromMap =\n (predicate: (call: ToolCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>\n (calls: ReadonlyArray<ToolCall>): ToolCallPlan =>\n splitApprovalDecisions(\n calls.map((call) => {\n if (!predicate(call)) return approve(call)\n const v = approvals.get(call.call_id)\n if (v === undefined) return reject(cancelled(call))\n return v.decision === \"approve\" ? approve(call) : reject(denied(call, v.reason))\n }),\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,mBAAmB,KAAK,YAA8B;AAEnE,MAAa,WAAW,SAAqC,iBAAiB,SAAS,EAAE,MAAM,CAAC;AAEhG,MAAa,UAAU,WACrB,iBAAiB,SAAS,EAAE,QAAQ,CAAC;AAEvC,MAAa,0BAA0B,cACrC,UAAU,QACP,KAAK,aACJ,SAAS,SAAS,aACd;CAAE,GAAG;CAAK,UAAU,CAAC,GAAG,IAAI,UAAU,SAAS,KAAK;CAAE,GACtD;CAAE,GAAG;CAAK,UAAU,CAAC,GAAG,IAAI,UAAU,SAAS,OAAO;CAAE,EAC9D;CAAE,UAAU,EAAE;CAAE,UAAU,EAAE;CAAE,CAC/B;AAEH,MAAa,qBAAqB,SAChC,UAAU,kBAAkB;CAC1B,SAAS,KAAK;CACd,MAAM,KAAK;CACX,WAAW,KAAK;CACjB,CAAC;;;;;;AAiBJ,MAAa,aACV,WAAwC,cAEvC,UAUA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,MAAM,OAAO,UAAU;CACrC,MAAM,WAAW,MAAM,QAAQ,SAAS,CAAC,UAAU,KAAK,CAAC;CAEzD,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;AAoBD,QAAO;EAAE;EAAU,WAlBD,OAAO,aAAa,MAAM,CAAC,KAC3C,OAAO,SACJ,SAAS;GACR,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,UAAO,OAAO,WACZ,SAAS,MAAM,EAAE,CAAC,KAChB,OAAO,KAAK,MACV,EAAE,aAAa,YAAY,QAAQ,KAAK,GAAG,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC,CAC1E,CACF,CACF;KAEH,EAAE,aAAa,aAAa,CAC7B,CAKyB;EAAE,kBAFL,OAAO,aAAwB,MAAM,IAAI,kBAAkB,CAEtC;EAAE;EAChD;AAWN,MAAa,WACV,WAAwC,eACxC,UACC,uBACE,MAAM,KAAK,SAAS;AAClB,KAAI,CAAC,UAAU,KAAK,CAAE,QAAO,QAAQ,KAAK;CAC1C,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,KAAI,MAAM,KAAA,EAAW,QAAO,OAAO,UAAU,KAAK,CAAC;AACnD,QAAO,EAAE,aAAa,YAAY,QAAQ,KAAK,GAAG,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC;EAChF,CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toolCallOutput, userText } from "../domain/Items.mjs";
|
|
2
2
|
import { fromEffectSchema, make, streaming } from "./Tool.mjs";
|
|
3
|
-
import { isFailure,
|
|
4
|
-
import { ToolEvent, isApprovalRequested,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { isFailure, isOk, toToolCallOutput } from "./ToolResult.mjs";
|
|
4
|
+
import { ToolEvent, isApprovalRequested, isOutput, isProgress } from "./ToolEvent.mjs";
|
|
5
|
+
import { run } from "./Toolkit.mjs";
|
|
6
|
+
import { fromMap, fromQueue } from "./Approval.mjs";
|
|
7
7
|
import { cancelAllPending, findUnansweredCalls, isReconciled } from "./HistoryCheck.mjs";
|
|
8
8
|
import { i as it, n as globalExpect, r as describe } from "../dist-DV5ISja1.mjs";
|
|
9
9
|
import { Effect, Queue, Schema, Stream } from "effect";
|
|
10
|
-
//#region src/tool/
|
|
10
|
+
//#region src/tool/Approval.test.ts
|
|
11
11
|
/**
|
|
12
12
|
* Tests for approval planners + history-reconciliation primitives. Exercises
|
|
13
13
|
* the full HITL + streaming-tool stack end-to-end by composing approval plans
|
|
14
|
-
* with `
|
|
14
|
+
* with `run`, with the four wire-shaped scenarios:
|
|
15
15
|
*
|
|
16
16
|
* 1. Approval : gated calls approved → tools execute, structured Values
|
|
17
17
|
* 2. Denial : gated calls denied → Failure(denied) results
|
|
@@ -88,28 +88,28 @@ const resultsFrom = (collected) => collected.filter(isOutput).map((e) => e.resul
|
|
|
88
88
|
const byCallId = (results) => new Map(results.map((r) => [r.call_id, r]));
|
|
89
89
|
const rejectedStream = (rejected) => Stream.fromIterable(rejected.map((result) => ToolEvent.Output({ result })));
|
|
90
90
|
const eventsFromApprovalMap = (approvals) => {
|
|
91
|
-
const plan =
|
|
92
|
-
return Stream.merge(
|
|
91
|
+
const plan = fromMap(isSensitive, approvals)(calls);
|
|
92
|
+
return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected));
|
|
93
93
|
};
|
|
94
|
-
const eventsFromDecision = (decision) => decision._tag === "Approved" ?
|
|
95
|
-
describe("
|
|
94
|
+
const eventsFromDecision = (decision) => decision._tag === "Approved" ? run(allTools, [decision.call]) : Stream.succeed(ToolEvent.Output({ result: decision.result }));
|
|
95
|
+
describe("fromMap + run", () => {
|
|
96
96
|
it("approval: all gated approved → tools execute, structured Values", async () => {
|
|
97
97
|
const approvals = new Map([["c2", { decision: "approve" }], ["c3", { decision: "approve" }]]);
|
|
98
98
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)));
|
|
99
99
|
const by = byCallId(resultsFrom(collected));
|
|
100
100
|
globalExpect(by.get("c1")).toMatchObject({
|
|
101
|
-
_tag: "
|
|
101
|
+
_tag: "Ok",
|
|
102
102
|
value: { count: 3 }
|
|
103
103
|
});
|
|
104
104
|
globalExpect(by.get("c2")).toMatchObject({
|
|
105
|
-
_tag: "
|
|
105
|
+
_tag: "Ok",
|
|
106
106
|
value: {
|
|
107
107
|
status: "sent",
|
|
108
108
|
delivered: 2
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
111
|
globalExpect(by.get("c3")).toMatchObject({
|
|
112
|
-
_tag: "
|
|
112
|
+
_tag: "Ok",
|
|
113
113
|
value: {
|
|
114
114
|
status: "dropped",
|
|
115
115
|
name: "prod"
|
|
@@ -126,7 +126,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
126
126
|
reason: "prod is sacred"
|
|
127
127
|
}]]);
|
|
128
128
|
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)));
|
|
129
|
-
globalExpect(collected.filter(
|
|
129
|
+
globalExpect(collected.filter(isProgress).filter((e) => e.tool === "bulk_email")).toHaveLength(0);
|
|
130
130
|
const by = byCallId(resultsFrom(collected));
|
|
131
131
|
globalExpect(by.get("c2")).toMatchObject({
|
|
132
132
|
_tag: "Failure",
|
|
@@ -142,7 +142,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
142
142
|
it("cancellation: missing verdicts → Failure(cancelled)", async () => {
|
|
143
143
|
const by = byCallId(resultsFrom(await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(/* @__PURE__ */ new Map())))));
|
|
144
144
|
globalExpect(by.get("c1")).toMatchObject({
|
|
145
|
-
_tag: "
|
|
145
|
+
_tag: "Ok",
|
|
146
146
|
value: { count: 3 }
|
|
147
147
|
});
|
|
148
148
|
globalExpect(by.get("c2")).toMatchObject({
|
|
@@ -158,11 +158,11 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
158
158
|
const approvals = new Map([["c2", { decision: "approve" }]]);
|
|
159
159
|
const by = byCallId(resultsFrom(await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))));
|
|
160
160
|
globalExpect(by.get("c1")).toMatchObject({
|
|
161
|
-
_tag: "
|
|
161
|
+
_tag: "Ok",
|
|
162
162
|
value: { count: 3 }
|
|
163
163
|
});
|
|
164
164
|
globalExpect(by.get("c2")).toMatchObject({
|
|
165
|
-
_tag: "
|
|
165
|
+
_tag: "Ok",
|
|
166
166
|
value: { status: "sent" }
|
|
167
167
|
});
|
|
168
168
|
globalExpect(by.get("c3")).toMatchObject({
|
|
@@ -171,7 +171,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
|
-
describe("
|
|
174
|
+
describe("run: graceful degradation", () => {
|
|
175
175
|
it("unknown tool name → Failure(unknown_tool); other calls still execute", async () => {
|
|
176
176
|
const callsWithBogus = [
|
|
177
177
|
fc("c1", "web_search", { query: "x" }),
|
|
@@ -179,21 +179,21 @@ describe("executeAll: graceful degradation", () => {
|
|
|
179
179
|
fc("c3", "delete_database", { name: "prod" })
|
|
180
180
|
];
|
|
181
181
|
const by = byCallId(resultsFrom(await Effect.runPromise(Stream.runCollect((() => {
|
|
182
|
-
const plan =
|
|
183
|
-
return Stream.merge(
|
|
182
|
+
const plan = fromMap(isSensitive, new Map([["c3", { decision: "approve" }]]))(callsWithBogus);
|
|
183
|
+
return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected));
|
|
184
184
|
})()))));
|
|
185
|
-
globalExpect(by.get("c1")).toMatchObject({ _tag: "
|
|
185
|
+
globalExpect(by.get("c1")).toMatchObject({ _tag: "Ok" });
|
|
186
186
|
globalExpect(by.get("c2")).toMatchObject({
|
|
187
187
|
_tag: "Failure",
|
|
188
188
|
kind: "unknown_tool"
|
|
189
189
|
});
|
|
190
190
|
globalExpect(by.get("c3")).toMatchObject({
|
|
191
|
-
_tag: "
|
|
191
|
+
_tag: "Ok",
|
|
192
192
|
value: { status: "dropped" }
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
|
-
describe("
|
|
196
|
+
describe("fromQueue + run", () => {
|
|
197
197
|
it("queue-driven: approve + deny resolve correctly with ApprovalRequested events", async () => {
|
|
198
198
|
const collected = await Effect.runPromise(Effect.gen(function* () {
|
|
199
199
|
const verdicts = yield* Queue.unbounded();
|
|
@@ -207,15 +207,15 @@ describe("fromVerdictQueue + executeAll", () => {
|
|
|
207
207
|
reason: "too risky"
|
|
208
208
|
});
|
|
209
209
|
const events = Stream.unwrap(Effect.gen(function* () {
|
|
210
|
-
const { approved, decisions,
|
|
211
|
-
return Stream.merge(
|
|
210
|
+
const { approved, decisions, approvalRequests } = yield* fromQueue(isSensitive, verdicts)(calls);
|
|
211
|
+
return Stream.merge(approvalRequests, Stream.merge(run(allTools, approved), decisions.pipe(Stream.flatMap(eventsFromDecision))));
|
|
212
212
|
}));
|
|
213
213
|
return yield* Stream.runCollect(events);
|
|
214
214
|
}));
|
|
215
215
|
globalExpect(collected.filter(isApprovalRequested)).toHaveLength(2);
|
|
216
216
|
const by = byCallId(resultsFrom(collected));
|
|
217
217
|
globalExpect(by.get("c2")).toMatchObject({
|
|
218
|
-
_tag: "
|
|
218
|
+
_tag: "Ok",
|
|
219
219
|
value: { status: "sent" }
|
|
220
220
|
});
|
|
221
221
|
globalExpect(by.get("c3")).toMatchObject({
|
|
@@ -228,7 +228,7 @@ describe("fromVerdictQueue + executeAll", () => {
|
|
|
228
228
|
describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
229
229
|
const orphan = fc("c99", "delete_database", { name: "prod" });
|
|
230
230
|
const answered = fc("c98", "web_search", { query: "x" });
|
|
231
|
-
const answeredOutput =
|
|
231
|
+
const answeredOutput = toolCallOutput("c98", JSON.stringify({ count: 0 }));
|
|
232
232
|
it("findUnansweredCalls returns only orphans", () => {
|
|
233
233
|
const unanswered = findUnansweredCalls([
|
|
234
234
|
userText("hi"),
|
|
@@ -242,7 +242,7 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
242
242
|
it("isReconciled is false when orphans exist, true otherwise", () => {
|
|
243
243
|
const stale = [userText("hi"), orphan];
|
|
244
244
|
globalExpect(isReconciled(stale)).toBe(false);
|
|
245
|
-
globalExpect(isReconciled([...stale, ...cancelAllPending(stale).map(
|
|
245
|
+
globalExpect(isReconciled([...stale, ...cancelAllPending(stale).map(toToolCallOutput)])).toBe(true);
|
|
246
246
|
});
|
|
247
247
|
it("cancelAllPending synthesizes one Failure(cancelled) per orphan", () => {
|
|
248
248
|
const closures = cancelAllPending([
|
|
@@ -261,20 +261,20 @@ describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
|
261
261
|
reason: "user moved on"
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
|
-
it("follow-up: map closures to
|
|
264
|
+
it("follow-up: map closures to ToolCallOutput before appending new user message", () => {
|
|
265
265
|
const stale = [userText("first request"), orphan];
|
|
266
266
|
const closures = cancelAllPending(stale, "user redirected");
|
|
267
267
|
globalExpect(findUnansweredCalls([
|
|
268
268
|
...stale,
|
|
269
|
-
...closures.map(
|
|
269
|
+
...closures.map(toToolCallOutput),
|
|
270
270
|
userText("never mind")
|
|
271
271
|
])).toHaveLength(0);
|
|
272
272
|
});
|
|
273
273
|
});
|
|
274
|
-
describe("
|
|
274
|
+
describe("toToolCallOutput", () => {
|
|
275
275
|
it("round-trips a Value result", () => {
|
|
276
|
-
const out =
|
|
277
|
-
_tag: "
|
|
276
|
+
const out = toToolCallOutput({
|
|
277
|
+
_tag: "Ok",
|
|
278
278
|
call_id: "c1",
|
|
279
279
|
tool: "web_search",
|
|
280
280
|
value: { count: 3 }
|
|
@@ -283,7 +283,7 @@ describe("toFunctionCallOutput", () => {
|
|
|
283
283
|
globalExpect(JSON.parse(out.output)).toEqual({ count: 3 });
|
|
284
284
|
});
|
|
285
285
|
it("round-trips a Failure result with reason", () => {
|
|
286
|
-
const out =
|
|
286
|
+
const out = toToolCallOutput({
|
|
287
287
|
_tag: "Failure",
|
|
288
288
|
call_id: "c2",
|
|
289
289
|
tool: "bulk_email",
|
|
@@ -296,7 +296,7 @@ describe("toFunctionCallOutput", () => {
|
|
|
296
296
|
});
|
|
297
297
|
});
|
|
298
298
|
it("round-trips a Failure result without reason (omits the field)", () => {
|
|
299
|
-
const out =
|
|
299
|
+
const out = toToolCallOutput({
|
|
300
300
|
_tag: "Failure",
|
|
301
301
|
call_id: "c3",
|
|
302
302
|
tool: "delete_database",
|
|
@@ -305,14 +305,14 @@ describe("toFunctionCallOutput", () => {
|
|
|
305
305
|
globalExpect(JSON.parse(out.output)).toEqual({ kind: "cancelled" });
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
|
-
describe("
|
|
308
|
+
describe("run", () => {
|
|
309
309
|
it("runs all calls passed to it", async () => {
|
|
310
|
-
const collected = await Effect.runPromise(Stream.runCollect(
|
|
310
|
+
const collected = await Effect.runPromise(Stream.runCollect(run(allTools, calls)));
|
|
311
311
|
globalExpect(collected.filter(isOutput)).toHaveLength(3);
|
|
312
|
-
globalExpect(collected.filter(isOutput).every((e) =>
|
|
312
|
+
globalExpect(collected.filter(isOutput).every((e) => isOk(e.result))).toBe(true);
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
315
|
//#endregion
|
|
316
316
|
export {};
|
|
317
317
|
|
|
318
|
-
//# sourceMappingURL=
|
|
318
|
+
//# sourceMappingURL=Approval.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Approval.test.mjs","names":["makeTool","Items.toolCallOutput","Items.userText"],"sources":["../../src/tool/Approval.test.ts"],"sourcesContent":["/**\n * Tests for approval planners + history-reconciliation primitives. Exercises\n * the full HITL + streaming-tool stack end-to-end by composing approval plans\n * with `run`, with the four wire-shaped scenarios:\n *\n * 1. Approval : gated calls approved → tools execute, structured Values\n * 2. Denial : gated calls denied → Failure(denied) results\n * 3. Cancellation : missing verdicts → Failure(cancelled) results\n * 4. Mixed + history : reconciliation via cancelAllPending\n *\n * Plus: hallucinated tool name (graceful Failure), unknown_tool kind.\n */\nimport { Effect, Queue, Schema, Stream } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport * as Items from \"../domain/Items.js\"\nimport { findUnansweredCalls, cancelAllPending, isReconciled } from \"./HistoryCheck.js\"\nimport { type ToolResult, isFailure, isOk, toToolCallOutput } from \"./ToolResult.js\"\nimport { type ApprovalMapEntry, type ApprovalDecision, fromMap, fromQueue } from \"./Approval.js\"\nimport { fromEffectSchema, make as makeTool, streaming } from \"./Tool.js\"\nimport { run } from \"./Toolkit.js\"\nimport { ToolEvent, isApprovalRequested, isProgress, isOutput } from \"./ToolEvent.js\"\n\n// ---------------------------------------------------------------------------\n// Three demo tools covering the matrix:\n// - web_search : streaming, no approval\n// - bulk_email : streaming, requires approval\n// - delete_database : non-streaming, requires approval\n// ---------------------------------------------------------------------------\n\nconst webSearch = streaming({\n name: \"web_search\",\n description: \"search\",\n inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),\n run: ({ query }) =>\n Stream.fromIterable([\n { url: \"a\", title: `${query} 1` },\n { url: \"b\", title: `${query} 2` },\n { url: \"c\", title: `${query} 3` },\n ]),\n finalize: (hits) => ({ count: hits.length }),\n})\n\nconst bulkEmail = streaming({\n name: \"bulk_email\",\n description: \"send\",\n inputSchema: fromEffectSchema(\n Schema.Struct({ recipients: Schema.Array(Schema.String), subject: Schema.String }),\n ),\n run: ({ recipients }) =>\n Stream.fromIterable(\n recipients.map((_, i) => ({\n type: \"progress\" as const,\n sent: i + 1,\n total: recipients.length,\n })),\n ),\n finalize: (events) => ({ status: \"sent\" as const, delivered: events.length }),\n})\n\nconst deleteDatabase = makeTool({\n name: \"delete_database\",\n description: \"drop\",\n inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),\n run: ({ name }) => Effect.succeed({ status: \"dropped\", name }),\n})\n\nconst allTools = [webSearch, bulkEmail, deleteDatabase]\nconst SENSITIVE = new Set([\"bulk_email\", \"delete_database\"])\nconst isSensitive = (call: Items.ToolCall) => SENSITIVE.has(call.name)\n\nconst fc = (call_id: string, name: string, args: unknown): Items.ToolCall => ({\n type: \"function_call\",\n call_id,\n name,\n arguments: JSON.stringify(args),\n})\n\nconst calls = [\n fc(\"c1\", \"web_search\", { query: \"effect\" }),\n fc(\"c2\", \"bulk_email\", { recipients: [\"a@x\", \"b@x\"], subject: \"Hi\" }),\n fc(\"c3\", \"delete_database\", { name: \"prod\" }),\n]\n\nconst resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolResult> =>\n collected.filter(isOutput).map((e) => e.result)\n\nconst byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))\n\nconst rejectedStream = (rejected: ReadonlyArray<ToolResult>) =>\n Stream.fromIterable(rejected.map((result) => ToolEvent.Output({ result })))\n\nconst eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {\n const plan = fromMap(isSensitive, approvals)(calls)\n return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))\n}\n\nconst eventsFromDecision = (decision: ApprovalDecision): Stream.Stream<ToolEvent> =>\n decision._tag === \"Approved\"\n ? run(allTools, [decision.call])\n : Stream.succeed(ToolEvent.Output({ result: decision.result }))\n\n// ---------------------------------------------------------------------------\n// fromMap: HTTP-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromMap + run\", () => {\n it(\"approval: all gated approved → tools execute, structured Values\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"approve\" }],\n [\"c3\", { decision: \"approve\" }],\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Ok\",\n value: { status: \"sent\", delivered: 2 },\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Ok\",\n value: { status: \"dropped\", name: \"prod\" },\n })\n\n // No ApprovalRequested events from the pure HTTP flow.\n expect(collected.filter(isApprovalRequested)).toHaveLength(0)\n })\n\n it(\"denial: gated denied → Failure(denied), no execution\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"deny\", reason: \"spam concern\" }],\n [\"c3\", { decision: \"deny\", reason: \"prod is sacred\" }],\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n\n // bulk_email never ran.\n expect(collected.filter(isProgress).filter((e) => e.tool === \"bulk_email\")).toHaveLength(0)\n\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"spam concern\",\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"prod is sacred\",\n })\n })\n\n it(\"cancellation: missing verdicts → Failure(cancelled)\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n })\n\n it(\"mixed: approve + deny + omit → all three kinds\", async () => {\n const approvals = new Map<string, ApprovalMapEntry>([\n [\"c2\", { decision: \"approve\" }],\n // c3 omitted → cancelled\n ])\n const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Ok\", value: { status: \"sent\" } })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Failure\", kind: \"cancelled\" })\n })\n})\n\n// ---------------------------------------------------------------------------\n// Graceful degradation: hallucinated tool name doesn't kill the turn.\n// ---------------------------------------------------------------------------\n\ndescribe(\"run: graceful degradation\", () => {\n it(\"unknown tool name → Failure(unknown_tool); other calls still execute\", async () => {\n const callsWithBogus = [\n fc(\"c1\", \"web_search\", { query: \"x\" }),\n fc(\"c2\", \"does_not_exist\", {}),\n fc(\"c3\", \"delete_database\", { name: \"prod\" }),\n ]\n const collected = await Effect.runPromise(\n Stream.runCollect(\n (() => {\n const plan = fromMap(\n isSensitive,\n new Map([[\"c3\", { decision: \"approve\" }]]),\n )(callsWithBogus)\n return Stream.merge(run(allTools, plan.approved), rejectedStream(plan.rejected))\n })(),\n ),\n )\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Ok\" })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"unknown_tool\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Ok\", value: { status: \"dropped\" } })\n })\n})\n\n// ---------------------------------------------------------------------------\n// fromQueue: WebSocket-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromQueue + run\", () => {\n it(\"queue-driven: approve + deny resolve correctly with ApprovalRequested events\", async () => {\n const collected = await Effect.runPromise(\n Effect.gen(function* () {\n const verdicts = yield* Queue.unbounded<{\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n }>()\n yield* Queue.offer(verdicts, { call_id: \"c2\", decision: \"approve\" })\n yield* Queue.offer(verdicts, {\n call_id: \"c3\",\n decision: \"deny\",\n reason: \"too risky\",\n })\n\n // Stream.unwrap supplies the Scope for fromQueue's router.\n const events = Stream.unwrap(\n Effect.gen(function* () {\n const { approved, decisions, approvalRequests } = yield* fromQueue(\n isSensitive,\n verdicts,\n )(calls)\n return Stream.merge(\n approvalRequests,\n Stream.merge(\n run(allTools, approved),\n decisions.pipe(Stream.flatMap(eventsFromDecision)),\n ),\n )\n }),\n )\n return yield* Stream.runCollect(events)\n }),\n )\n\n expect(collected.filter(isApprovalRequested)).toHaveLength(2)\n\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Ok\", value: { status: \"sent\" } })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Failure\",\n kind: \"denied\",\n reason: \"too risky\",\n })\n })\n})\n\n// ---------------------------------------------------------------------------\n// History reconciliation\n// ---------------------------------------------------------------------------\n\ndescribe(\"findUnansweredCalls / cancelAllPending / isReconciled\", () => {\n const orphan = fc(\"c99\", \"delete_database\", { name: \"prod\" })\n const answered = fc(\"c98\", \"web_search\", { query: \"x\" })\n const answeredOutput = Items.toolCallOutput(\"c98\", JSON.stringify({ count: 0 }))\n\n it(\"findUnansweredCalls returns only orphans\", () => {\n const history = [Items.userText(\"hi\"), answered, orphan, answeredOutput]\n const unanswered = findUnansweredCalls(history)\n expect(unanswered).toHaveLength(1)\n expect(unanswered[0]!.call_id).toBe(\"c99\")\n })\n\n it(\"isReconciled is false when orphans exist, true otherwise\", () => {\n const stale = [Items.userText(\"hi\"), orphan]\n expect(isReconciled(stale)).toBe(false)\n const reconciled = [...stale, ...cancelAllPending(stale).map(toToolCallOutput)]\n expect(isReconciled(reconciled)).toBe(true)\n })\n\n it(\"cancelAllPending synthesizes one Failure(cancelled) per orphan\", () => {\n const history = [Items.userText(\"hi\"), answered, orphan, answeredOutput]\n const closures = cancelAllPending(history, \"user moved on\")\n expect(closures).toHaveLength(1)\n const c = closures[0]!\n expect(isFailure(c)).toBe(true)\n expect(c).toMatchObject({\n _tag: \"Failure\",\n call_id: \"c99\",\n kind: \"cancelled\",\n reason: \"user moved on\",\n })\n })\n\n it(\"follow-up: map closures to ToolCallOutput before appending new user message\", () => {\n const stale = [Items.userText(\"first request\"), orphan]\n const closures = cancelAllPending(stale, \"user redirected\")\n const reconciled = [...stale, ...closures.map(toToolCallOutput), Items.userText(\"never mind\")]\n expect(findUnansweredCalls(reconciled)).toHaveLength(0)\n })\n})\n\n// ---------------------------------------------------------------------------\n// Wire conversion\n// ---------------------------------------------------------------------------\n\ndescribe(\"toToolCallOutput\", () => {\n it(\"round-trips a Value result\", () => {\n const r: ToolResult = {\n _tag: \"Ok\",\n call_id: \"c1\",\n tool: \"web_search\",\n value: { count: 3 },\n }\n const out = toToolCallOutput(r)\n expect(out.call_id).toBe(\"c1\")\n expect(JSON.parse(out.output)).toEqual({ count: 3 })\n })\n\n it(\"round-trips a Failure result with reason\", () => {\n const r: ToolResult = {\n _tag: \"Failure\",\n call_id: \"c2\",\n tool: \"bulk_email\",\n kind: \"denied\",\n reason: \"spam concern\",\n }\n const out = toToolCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ kind: \"denied\", reason: \"spam concern\" })\n })\n\n it(\"round-trips a Failure result without reason (omits the field)\", () => {\n const r: ToolResult = {\n _tag: \"Failure\",\n call_id: \"c3\",\n tool: \"delete_database\",\n kind: \"cancelled\",\n }\n const out = toToolCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ kind: \"cancelled\" })\n })\n})\n\n// ---------------------------------------------------------------------------\n// run\n// ---------------------------------------------------------------------------\n\ndescribe(\"run\", () => {\n it(\"runs all calls passed to it\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(run(allTools, calls)))\n expect(collected.filter(isOutput)).toHaveLength(3)\n expect(collected.filter(isOutput).every((e) => isOk(e.result))).toBe(true)\n })\n})\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ToolResult } from "./
|
|
1
|
+
import { o as HistoryItem, v as ToolCall } from "../Items-pXR5BAgW.mjs";
|
|
2
|
+
import { ToolResult } from "./ToolResult.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/tool/HistoryCheck.d.ts
|
|
5
5
|
declare namespace HistoryCheck_d_exports {
|
|
@@ -10,18 +10,18 @@ declare namespace HistoryCheck_d_exports {
|
|
|
10
10
|
* `function_call_output` later in `history` (correlated by `call_id`).
|
|
11
11
|
* Empty result = history is provider-submittable from this invariant.
|
|
12
12
|
*/
|
|
13
|
-
declare const findUnansweredCalls: (history: ReadonlyArray<
|
|
13
|
+
declare const findUnansweredCalls: (history: ReadonlyArray<HistoryItem>) => ReadonlyArray<ToolCall>;
|
|
14
14
|
/** Cheap predicate: is this history submittable to a provider? */
|
|
15
|
-
declare const isReconciled: (history: ReadonlyArray<
|
|
15
|
+
declare const isReconciled: (history: ReadonlyArray<HistoryItem>) => boolean;
|
|
16
16
|
/**
|
|
17
17
|
* Synthesize cancellation results for every unanswered call. Caller maps
|
|
18
|
-
* via `
|
|
18
|
+
* via `toToolCallOutput` and appends to history before submitting.
|
|
19
19
|
*
|
|
20
20
|
* Use when: a new user message arrives mid-approval; an approval timer
|
|
21
21
|
* fires; a persisted checkpoint contains orphans (crash recovery); a
|
|
22
22
|
* stateless HTTP server reconstructed history from a stale checkpoint.
|
|
23
23
|
*/
|
|
24
|
-
declare const cancelAllPending: (history: ReadonlyArray<
|
|
24
|
+
declare const cancelAllPending: (history: ReadonlyArray<HistoryItem>, reason?: string) => ReadonlyArray<ToolResult>;
|
|
25
25
|
//#endregion
|
|
26
26
|
export { cancelAllPending, findUnansweredCalls, isReconciled, HistoryCheck_d_exports as t };
|
|
27
27
|
//# sourceMappingURL=HistoryCheck.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HistoryCheck.d.mts","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"HistoryCheck.d.mts","names":[],"sources":["../../src/tool/HistoryCheck.ts"],"mappings":";;;;;;;;;;;;cAoBa,mBAAA,GACX,OAAA,EAAS,aAAA,CAAc,WAAA,MACtB,aAAA,CAAc,QAAA;;cAMJ,YAAA,GAAgB,OAAA,EAAS,aAAA,CAAc,WAAA;;;;;AAApD;;;;cAWa,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,WAAA,GACvB,MAAA,cACC,aAAA,CAAc,UAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { cancelled } from "./
|
|
2
|
+
import { isToolCall, isToolCallOutput } from "../domain/Items.mjs";
|
|
3
|
+
import { cancelled } from "./ToolResult.mjs";
|
|
4
4
|
//#region src/tool/HistoryCheck.ts
|
|
5
5
|
/**
|
|
6
6
|
* History-consistency primitives. Useful even WITHOUT HITL.
|
|
@@ -25,14 +25,14 @@ var HistoryCheck_exports = /* @__PURE__ */ __exportAll({
|
|
|
25
25
|
* Empty result = history is provider-submittable from this invariant.
|
|
26
26
|
*/
|
|
27
27
|
const findUnansweredCalls = (history) => {
|
|
28
|
-
const answered = new Set(history.filter(
|
|
29
|
-
return history.filter(
|
|
28
|
+
const answered = new Set(history.filter(isToolCallOutput).map((o) => o.call_id));
|
|
29
|
+
return history.filter(isToolCall).filter((c) => !answered.has(c.call_id));
|
|
30
30
|
};
|
|
31
31
|
/** Cheap predicate: is this history submittable to a provider? */
|
|
32
32
|
const isReconciled = (history) => findUnansweredCalls(history).length === 0;
|
|
33
33
|
/**
|
|
34
34
|
* Synthesize cancellation results for every unanswered call. Caller maps
|
|
35
|
-
* via `
|
|
35
|
+
* via `toToolCallOutput` and appends to history before submitting.
|
|
36
36
|
*
|
|
37
37
|
* Use when: a new user message arrives mid-approval; an approval timer
|
|
38
38
|
* fires; a persisted checkpoint contains orphans (crash recovery); a
|
|
@@ -1 +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 {
|
|
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 { type ToolCall, type HistoryItem, isToolCall, isToolCallOutput } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled } from \"./ToolResult.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<HistoryItem>,\n): ReadonlyArray<ToolCall> => {\n const answered = new Set(history.filter(isToolCallOutput).map((o) => o.call_id))\n return history.filter(isToolCall).filter((c) => !answered.has(c.call_id))\n}\n\n/** Cheap predicate: is this history submittable to a provider? */\nexport const isReconciled = (history: ReadonlyArray<HistoryItem>): boolean =>\n findUnansweredCalls(history).length === 0\n\n/**\n * Synthesize cancellation results for every unanswered call. Caller maps\n * via `toToolCallOutput` 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<HistoryItem>,\n reason?: string,\n): ReadonlyArray<ToolResult> => findUnansweredCalls(history).map((call) => cancelled(call, reason))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,uBACX,YAC4B;CAC5B,MAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,iBAAiB,CAAC,KAAK,MAAM,EAAE,QAAQ,CAAC;AAChF,QAAO,QAAQ,OAAO,WAAW,CAAC,QAAQ,MAAM,CAAC,SAAS,IAAI,EAAE,QAAQ,CAAC;;;AAI3E,MAAa,gBAAgB,YAC3B,oBAAoB,QAAQ,CAAC,WAAW;;;;;;;;;AAU1C,MAAa,oBACX,SACA,WAC8B,oBAAoB,QAAQ,CAAC,KAAK,SAAS,UAAU,MAAM,OAAO,CAAC"}
|
package/dist/tool/Tool.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as Tool, c as ToolInputSchema, d as fromEffectSchema, f as fromStandardSchema, g as toDescriptors, h as streaming, i as StreamingTool, m as make, n as
|
|
2
|
-
export {
|
|
1
|
+
import { a as Tool, c as ToolInputSchema, d as fromEffectSchema, f as fromStandardSchema, g as toDescriptors, h as streaming, i as StreamingTool, m as make, n as AnyStreamingTool, o as ToolDescriptor, p as isStreamingTool, r as AnyTool, s as ToolError, t as AnyPlainTool, u as execute } from "../Tool-DP-AwKOf.mjs";
|
|
2
|
+
export { AnyPlainTool, AnyStreamingTool, AnyTool, StreamingTool, Tool, ToolDescriptor, ToolError, ToolInputSchema, execute, fromEffectSchema, fromStandardSchema, isStreamingTool, make, streaming, toDescriptors };
|
package/dist/tool/Tool.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { toolCallOutput } from "../domain/Items.mjs";
|
|
3
3
|
import { Effect, Schema } from "effect";
|
|
4
4
|
//#region src/tool/Tool.ts
|
|
5
5
|
var Tool_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -48,8 +48,8 @@ const streaming = (spec) => ({
|
|
|
48
48
|
const isStreamingTool = (t) => "_kind" in t && t._kind === "streaming";
|
|
49
49
|
/**
|
|
50
50
|
* Render any-kind tools (mixed plain and streaming) to provider-agnostic
|
|
51
|
-
* descriptors.
|
|
52
|
-
*
|
|
51
|
+
* descriptors. Accepts the union type so a single list can carry both
|
|
52
|
+
* plain and streaming tools.
|
|
53
53
|
*/
|
|
54
54
|
const toDescriptors = (tools) => tools.map((tool) => {
|
|
55
55
|
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({ target: "draft-2020-12" });
|
|
@@ -88,7 +88,7 @@ const execute = (tool, call) => Effect.gen(function* () {
|
|
|
88
88
|
cause: result.issues
|
|
89
89
|
});
|
|
90
90
|
const output = yield* tool.run(result.value).pipe(Effect.mapError(toToolError(call, tool.name, "Tool execution failed")));
|
|
91
|
-
return
|
|
91
|
+
return toolCallOutput(call.call_id, JSON.stringify(output));
|
|
92
92
|
});
|
|
93
93
|
//#endregion
|
|
94
94
|
export { ToolError, execute, fromEffectSchema, fromStandardSchema, isStreamingTool, make, streaming, Tool_exports as t, toDescriptors };
|
package/dist/tool/Tool.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tool.mjs","names":[],"sources":["../../src/tool/Tool.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Effect, Schema, Stream } from \"effect\"\nimport type {
|
|
1
|
+
{"version":3,"file":"Tool.mjs","names":[],"sources":["../../src/tool/Tool.ts"],"sourcesContent":["import type { StandardJSONSchemaV1, StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { Effect, Schema, Stream } from \"effect\"\nimport type { ToolCall, ToolCallOutput } from \"../domain/Items.js\"\nimport { toolCallOutput } from \"../domain/Items.js\"\n\nexport class ToolError extends Schema.TaggedErrorClass<ToolError>(\"@betalyra/effect-uai/ToolError\")(\n \"ToolError\",\n {\n call_id: Schema.String,\n tool: Schema.String,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n },\n) {}\n\n/**\n * Schemas accepted on `Tool.inputSchema`. Must implement both Standard\n * Schema validation and JSON Schema conversion (for rendering tool\n * descriptors to provider request bodies).\n *\n * Any Standard-Schema-compliant library that exposes both interfaces\n * works directly: Zod 4+, Valibot, ArkType, Effect Schema (after\n * `fromEffectSchema`), etc.\n */\nexport type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input> &\n StandardJSONSchemaV1<unknown, Input>\n\n/**\n * Convenience wrapper for Effect Schema users - adds both the\n * `validate` and `jsonSchema` extensions to a plain Effect Schema so it\n * can be used as a `Tool.inputSchema`.\n */\nexport const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(\n schema: S,\n): S & ToolInputSchema<S[\"Type\"]> =>\n Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)) as unknown as S &\n ToolInputSchema<S[\"Type\"]>\n\n/**\n * Use any schema library that implements both Standard Schema (validation)\n * and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.\n * Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.\n *\n * Effect Schema doesn't implement Standard JSON Schema natively — use\n * `fromEffectSchema` for those.\n *\n * The intersection constraint catches missing interfaces at compile time:\n * a Zod v3 schema (no Standard JSON Schema) produces a precise type error\n * pointing at the missing interface rather than a runtime surprise. The\n * helper itself is a thin type-narrowing identity — schemas that satisfy\n * both standards already structurally satisfy `ToolInputSchema`; the\n * helper makes the input type inference explicit at the call site.\n */\nexport const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(\n schema: S,\n): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>\n schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>\n\nexport type Tool<Name extends string, Input, Output, R = never> = {\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly run: (input: Input) => Effect.Effect<Output, unknown, R>\n /**\n * Whether the provider should render this tool with its strict-mode\n * flag (OpenAI's `strict: true`, etc). Default: true. The framework\n * never rewrites the schema; if the rendered JSON Schema isn't\n * compatible, the provider returns an error.\n */\n readonly strict?: boolean\n}\n\n/**\n * Provider-agnostic tool descriptor. Each provider maps `inputSchema`\n * to its own wire field (OpenAI → `parameters`, Anthropic →\n * `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.\n */\nexport type ToolDescriptor = {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly strict?: boolean\n}\n\nexport const make = <Name extends string, Input, Output, R = never>(\n spec: Tool<Name, Input, Output, R>,\n): Tool<Name, Input, Output, R> => spec\n\n// ---------------------------------------------------------------------------\n// Streaming tools\n//\n// `run` returns a `Stream<Event>` instead of an `Effect<Output>`. Events\n// flow through to the consumer as `ToolEvent.Progress`s in real time;\n// at end-of-stream `finalize(events)` reduces them to the model-facing\n// `Output`. Sub-agents, slow downloads with progress, recipe streamers.\n// ---------------------------------------------------------------------------\n\nexport type StreamingTool<Name extends string, Input, Event, Output, R = never> = {\n readonly _kind: \"streaming\"\n readonly name: Name\n readonly description: string\n readonly inputSchema: ToolInputSchema<Input>\n readonly run: (input: Input) => Stream.Stream<Event, unknown, R>\n readonly finalize: (events: ReadonlyArray<Event>) => Output\n readonly strict?: boolean\n}\n\nexport const streaming = <Name extends string, Input, Event, Output, R = never>(\n spec: Omit<StreamingTool<Name, Input, Event, Output, R>, \"_kind\">,\n): StreamingTool<Name, Input, Event, Output, R> => ({ _kind: \"streaming\", ...spec })\n\nexport type AnyStreamingTool<R = any> = StreamingTool<string, any, any, any, R>\nexport type AnyPlainTool<R = any> = Tool<string, any, any, R>\nexport type AnyTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>\n\nexport const isStreamingTool = <R>(t: AnyTool<R>): t is AnyStreamingTool<R> =>\n \"_kind\" in t && t._kind === \"streaming\"\n\n/**\n * Render any-kind tools (mixed plain and streaming) to provider-agnostic\n * descriptors. Accepts the union type so a single list can carry both\n * plain and streaming tools.\n */\nexport const toDescriptors = <R>(tools: ReadonlyArray<AnyTool<R>>): ReadonlyArray<ToolDescriptor> =>\n tools.map((tool) => {\n const inputSchema = tool.inputSchema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n })\n return tool.strict !== undefined\n ? { name: tool.name, description: tool.description, inputSchema, strict: tool.strict }\n : { name: tool.name, description: tool.description, inputSchema }\n })\n\nconst toToolError = (call: ToolCall, toolName: string, message: string) => (cause: unknown) =>\n new ToolError({ call_id: call.call_id, tool: toolName, message, cause })\n\n/**\n * Decode and validate the JSON arguments of a function_call against the\n * tool's input schema, run the tool, and serialize the output into a\n * function_call_output item.\n */\nexport const execute = <Name extends string, Input, Output, R>(\n tool: Tool<Name, Input, Output, R>,\n call: ToolCall,\n): Effect.Effect<ToolCallOutput, ToolError, R> =>\n Effect.gen(function* () {\n const parsed = yield* Effect.try({\n try: () => JSON.parse(call.arguments) as unknown,\n catch: toToolError(call, tool.name, \"Failed to parse JSON arguments\"),\n })\n\n const result = yield* Effect.promise(() =>\n Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n )\n if (result.issues !== undefined) {\n return yield* new ToolError({\n call_id: call.call_id,\n tool: tool.name,\n message: \"Tool input failed schema validation\",\n cause: result.issues,\n })\n }\n\n const output = yield* tool\n .run(result.value)\n .pipe(Effect.mapError(toToolError(call, tool.name, \"Tool execution failed\")))\n return toolCallOutput(call.call_id, JSON.stringify(output))\n })\n"],"mappings":";;;;;;;;;;;;;;AAKA,IAAa,YAAb,cAA+B,OAAO,iBAA4B,iCAAiC,CACjG,aACA;CACE,SAAS,OAAO;CAChB,MAAM,OAAO;CACb,SAAS,OAAO;CAChB,OAAO,OAAO,SAAS,OAAO,QAAQ;CACvC,CACF,CAAC;;;;;;AAmBF,MAAa,oBACX,WAEA,OAAO,uBAAuB,OAAO,mBAAmB,OAAO,CAAC;;;;;;;;;;;;;;;;AAkBlE,MAAa,sBACX,WAEA;AA4BF,MAAa,QACX,SACiC;AAqBnC,MAAa,aACX,UACkD;CAAE,OAAO;CAAa,GAAG;CAAM;AAMnF,MAAa,mBAAsB,MACjC,WAAW,KAAK,EAAE,UAAU;;;;;;AAO9B,MAAa,iBAAoB,UAC/B,MAAM,KAAK,SAAS;CAClB,MAAM,cAAc,KAAK,YAAY,aAAa,WAAW,MAAM,EACjE,QAAQ,iBACT,CAAC;AACF,QAAO,KAAK,WAAW,KAAA,IACnB;EAAE,MAAM,KAAK;EAAM,aAAa,KAAK;EAAa;EAAa,QAAQ,KAAK;EAAQ,GACpF;EAAE,MAAM,KAAK;EAAM,aAAa,KAAK;EAAa;EAAa;EACnE;AAEJ,MAAM,eAAe,MAAgB,UAAkB,aAAqB,UAC1E,IAAI,UAAU;CAAE,SAAS,KAAK;CAAS,MAAM;CAAU;CAAS;CAAO,CAAC;;;;;;AAO1E,MAAa,WACX,MACA,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,OAAO,IAAI;EAC/B,WAAW,KAAK,MAAM,KAAK,UAAU;EACrC,OAAO,YAAY,MAAM,KAAK,MAAM,iCAAiC;EACtE,CAAC;CAEF,MAAM,SAAS,OAAO,OAAO,cAC3B,QAAQ,QAAQ,KAAK,YAAY,aAAa,SAAS,OAAO,CAAC,CAChE;AACD,KAAI,OAAO,WAAW,KAAA,EACpB,QAAO,OAAO,IAAI,UAAU;EAC1B,SAAS,KAAK;EACd,MAAM,KAAK;EACX,SAAS;EACT,OAAO,OAAO;EACf,CAAC;CAGJ,MAAM,SAAS,OAAO,KACnB,IAAI,OAAO,MAAM,CACjB,KAAK,OAAO,SAAS,YAAY,MAAM,KAAK,MAAM,wBAAwB,CAAC,CAAC;AAC/E,QAAO,eAAe,KAAK,SAAS,KAAK,UAAU,OAAO,CAAC;EAC3D"}
|