@effect-uai/core 0.2.0 → 0.4.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/{AiError-CqmYjXyx.d.mts → AiError-csR8Bhxx.d.mts} +26 -4
- package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-csR8Bhxx.d.mts.map} +1 -1
- package/dist/Audio-BfCTGnH3.d.mts +61 -0
- package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
- package/dist/Image-DxyXqzAM.d.mts +61 -0
- package/dist/Image-DxyXqzAM.d.mts.map +1 -0
- package/dist/{Items-D1C2686t.d.mts → Items-Hg5AsYxl.d.mts} +132 -80
- package/dist/Items-Hg5AsYxl.d.mts.map +1 -0
- package/dist/Media-D_CpcM1Z.d.mts +57 -0
- package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
- package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-Cl41C56K.d.mts} +5 -5
- package/dist/StructuredFormat-Cl41C56K.d.mts.map +1 -0
- package/dist/{Tool-5wxOCuOh.d.mts → Tool-B8B5qVEy.d.mts} +13 -13
- package/dist/Tool-B8B5qVEy.d.mts.map +1 -0
- package/dist/{Turn-Bi83du4I.d.mts → Turn-7geUcKsf.d.mts} +5 -11
- package/dist/Turn-7geUcKsf.d.mts.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/dist-DV5ISja1.mjs +13782 -0
- package/dist/dist-DV5ISja1.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +19 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +2 -0
- package/dist/domain/Audio.mjs +14 -0
- package/dist/domain/Audio.mjs.map +1 -0
- package/dist/domain/Image.d.mts +2 -0
- package/dist/domain/Image.mjs +58 -0
- package/dist/domain/Image.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -42
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -0
- package/dist/domain/Media.mjs +14 -0
- package/dist/domain/Media.mjs.map +1 -0
- package/dist/domain/Music.d.mts +116 -0
- package/dist/domain/Music.d.mts.map +1 -0
- package/dist/domain/Music.mjs +29 -0
- package/dist/domain/Music.mjs.map +1 -0
- package/dist/domain/Transcript.d.mts +95 -0
- package/dist/domain/Transcript.d.mts.map +1 -0
- package/dist/domain/Transcript.mjs +22 -0
- package/dist/domain/Transcript.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +1 -1
- package/dist/domain/Turn.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +107 -0
- package/dist/embedding-model/Embedding.d.mts.map +1 -0
- package/dist/embedding-model/Embedding.mjs +18 -0
- package/dist/embedding-model/Embedding.mjs.map +1 -0
- package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
- package/dist/embedding-model/EmbeddingModel.mjs +17 -0
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
- package/dist/index.d.mts +21 -7
- package/dist/index.mjs +16 -2
- package/dist/language-model/LanguageModel.d.mts +12 -20
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +3 -20
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +31 -7
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -6
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -0
- package/dist/loop/Loop.test.mjs +411 -0
- package/dist/loop/Loop.test.mjs.map +1 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
- package/dist/math/Vector.d.mts +47 -0
- package/dist/math/Vector.d.mts.map +1 -0
- package/dist/math/Vector.mjs +117 -0
- package/dist/math/Vector.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.d.mts +77 -0
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
- package/dist/music-generator/MusicGenerator.mjs +51 -0
- package/dist/music-generator/MusicGenerator.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
- package/dist/music-generator/MusicGenerator.test.mjs +154 -0
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +2 -2
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
- package/dist/streaming/JSONL.d.mts +10 -3
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +13 -2
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -0
- package/dist/streaming/JSONL.test.mjs +70 -0
- package/dist/streaming/JSONL.test.mjs.map +1 -0
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/SSE.d.mts +2 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -0
- package/dist/streaming/SSE.test.mjs +72 -0
- package/dist/streaming/SSE.test.mjs.map +1 -0
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +39 -0
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
- package/dist/testing/MockMusicGenerator.mjs +96 -0
- package/dist/testing/MockMusicGenerator.mjs.map +1 -0
- package/dist/testing/MockProvider.d.mts +6 -6
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
- package/dist/testing/MockTranscriber.d.mts +37 -0
- package/dist/testing/MockTranscriber.d.mts.map +1 -0
- package/dist/testing/MockTranscriber.mjs +77 -0
- package/dist/testing/MockTranscriber.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -3
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +7 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Outcome.d.mts +138 -2
- package/dist/tool/Outcome.d.mts.map +1 -0
- package/dist/tool/Outcome.mjs +32 -10
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +11 -8
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +10 -1
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.d.mts +1 -0
- package/dist/tool/Resolvers.test.mjs +317 -0
- package/dist/tool/Resolvers.test.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +1 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +151 -2
- package/dist/tool/ToolEvent.d.mts.map +1 -0
- package/dist/tool/ToolEvent.mjs +30 -4
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +19 -10
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +5 -5
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -0
- package/dist/tool/Toolkit.test.mjs +113 -0
- package/dist/tool/Toolkit.test.mjs.map +1 -0
- package/dist/transcriber/Transcriber.d.mts +101 -0
- package/dist/transcriber/Transcriber.d.mts.map +1 -0
- package/dist/transcriber/Transcriber.mjs +49 -0
- package/dist/transcriber/Transcriber.mjs.map +1 -0
- package/dist/transcriber/Transcriber.test.d.mts +1 -0
- package/dist/transcriber/Transcriber.test.mjs +130 -0
- package/dist/transcriber/Transcriber.test.mjs.map +1 -0
- package/package.json +65 -13
- package/src/domain/AiError.ts +21 -0
- package/src/domain/Audio.ts +88 -0
- package/src/domain/Image.ts +75 -0
- package/src/domain/Items.ts +18 -47
- package/src/domain/Media.ts +61 -0
- package/src/domain/Music.ts +121 -0
- package/src/domain/Transcript.ts +83 -0
- package/src/embedding-model/Embedding.ts +117 -0
- package/src/embedding-model/EmbeddingModel.ts +107 -0
- package/src/index.ts +15 -1
- package/src/language-model/LanguageModel.ts +2 -22
- package/src/loop/Loop.test.ts +114 -2
- package/src/loop/Loop.ts +69 -5
- package/src/math/Vector.ts +138 -0
- package/src/music-generator/MusicGenerator.test.ts +170 -0
- package/src/music-generator/MusicGenerator.ts +123 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
- package/src/streaming/JSONL.ts +12 -0
- package/src/streaming/SSE.ts +1 -1
- package/src/structured-format/StructuredFormat.ts +2 -2
- package/src/testing/MockMusicGenerator.ts +170 -0
- package/src/testing/MockProvider.ts +2 -2
- package/src/testing/MockSpeechSynthesizer.ts +165 -0
- package/src/testing/MockTranscriber.ts +139 -0
- package/src/tool/HistoryCheck.ts +2 -5
- package/src/tool/Outcome.ts +36 -36
- package/src/tool/Resolvers.test.ts +11 -35
- package/src/tool/Resolvers.ts +5 -14
- package/src/tool/Tool.ts +9 -9
- package/src/tool/ToolEvent.ts +28 -24
- package/src/tool/Toolkit.test.ts +97 -2
- package/src/tool/Toolkit.ts +57 -33
- package/src/transcriber/Transcriber.test.ts +125 -0
- package/src/transcriber/Transcriber.ts +127 -0
- package/dist/Items-D1C2686t.d.mts.map +0 -1
- package/dist/Outcome-GiaNvt7i.d.mts +0 -32
- package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
- package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
- package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
- package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
- package/dist/Turn-Bi83du4I.d.mts.map +0 -1
- package/dist/match/Match.d.mts +0 -16
- package/dist/match/Match.d.mts.map +0 -1
- package/dist/match/Match.mjs +0 -15
- package/dist/match/Match.mjs.map +0 -1
- package/src/match/Match.ts +0 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Resolvers.mjs","names":[],"sources":["../../src/tool/Resolvers.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.executeAll`.\n */\nimport { Deferred, Effect, Queue, Scope, Stream } from \"effect\"\nimport type { FunctionCall } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled, denied } from \"./Outcome.js\"\nimport type { ToolEvent } from \"./ToolEvent.js\"\n\nexport
|
|
1
|
+
{"version":3,"file":"Resolvers.mjs","names":[],"sources":["../../src/tool/Resolvers.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.executeAll`.\n */\nimport { Deferred, Effect, Queue, Scope, Stream } from \"effect\"\nimport type { FunctionCall } from \"../domain/Items.js\"\nimport { type ToolResult, cancelled, denied } from \"./Outcome.js\"\nimport type { ToolEvent } from \"./ToolEvent.js\"\n\nexport type ToolCallPlan = {\n readonly approved: ReadonlyArray<FunctionCall>\n readonly rejected: ReadonlyArray<ToolResult>\n}\n\nexport type ToolCallDecision =\n | { readonly _tag: \"Approved\"; readonly call: FunctionCall }\n | { readonly _tag: \"Rejected\"; readonly result: ToolResult }\n\nexport const approve = (call: FunctionCall): ToolCallDecision => ({\n _tag: \"Approved\",\n call,\n})\n\nexport const reject = (result: ToolResult): ToolCallDecision => ({\n _tag: \"Rejected\",\n result,\n})\n\nexport const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision>): 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: FunctionCall): ToolEvent => ({\n _tag: \"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 `ToolCallDecision` when their matching verdict arrives.\n */\nexport const fromVerdictQueue =\n (predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>\n (\n calls: ReadonlyArray<FunctionCall>,\n ): Effect.Effect<\n {\n readonly approved: ReadonlyArray<FunctionCall>\n readonly decisions: Stream.Stream<ToolCallDecision>\n readonly announce: 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 announce = Stream.fromIterable<ToolEvent>(gated.map(approvalRequested))\n\n return { approved, decisions, 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 (predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>\n (calls: ReadonlyArray<FunctionCall>): ToolCallPlan =>\n splitToolCallDecisions(\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":";;;;;;;;;;;;;;;;;;;AAqBA,MAAa,WAAW,UAA0C;CAChE,MAAM;CACN;CACD;AAED,MAAa,UAAU,YAA0C;CAC/D,MAAM;CACN;CACD;AAED,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,UAAmC;CACnE,MAAM;CACN,SAAS,KAAK;CACd,MAAM,KAAK;CACX,WAAW,KAAK;CACjB;;;;;;AAiBD,MAAa,oBACV,WAA4C,cAE3C,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,UAFb,OAAO,aAAwB,MAAM,IAAI,kBAAkB,CAEtC;EAAE;EACxC;AAWN,MAAa,mBACV,WAA4C,eAC5C,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 { };
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { functionCallOutput, userText } from "../domain/Items.mjs";
|
|
2
|
+
import { fromEffectSchema, make, streaming } from "./Tool.mjs";
|
|
3
|
+
import { isFailure, isValue, toFunctionCallOutput } from "./Outcome.mjs";
|
|
4
|
+
import { isApprovalRequested, isIntermediate, isOutput } from "./ToolEvent.mjs";
|
|
5
|
+
import { executeAll, outputEvent, outputEvents } from "./Toolkit.mjs";
|
|
6
|
+
import { fromApprovalMap, fromVerdictQueue } from "./Resolvers.mjs";
|
|
7
|
+
import { cancelAllPending, findUnansweredCalls, isReconciled } from "./HistoryCheck.mjs";
|
|
8
|
+
import { i as it, n as globalExpect, r as describe } from "../dist-DV5ISja1.mjs";
|
|
9
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
10
|
+
//#region src/tool/Resolvers.test.ts
|
|
11
|
+
/**
|
|
12
|
+
* Tests for approval planners + history-reconciliation primitives. Exercises
|
|
13
|
+
* the full HITL + streaming-tool stack end-to-end by composing approval plans
|
|
14
|
+
* with `executeAll`, with the four wire-shaped scenarios:
|
|
15
|
+
*
|
|
16
|
+
* 1. Approval : gated calls approved → tools execute, structured Values
|
|
17
|
+
* 2. Denial : gated calls denied → Failure(denied) results
|
|
18
|
+
* 3. Cancellation : missing verdicts → Failure(cancelled) results
|
|
19
|
+
* 4. Mixed + history : reconciliation via cancelAllPending
|
|
20
|
+
*
|
|
21
|
+
* Plus: hallucinated tool name (graceful Failure), unknown_tool kind.
|
|
22
|
+
*/
|
|
23
|
+
const allTools = [
|
|
24
|
+
streaming({
|
|
25
|
+
name: "web_search",
|
|
26
|
+
description: "search",
|
|
27
|
+
inputSchema: fromEffectSchema(Schema.Struct({ query: Schema.String })),
|
|
28
|
+
run: ({ query }) => Stream.fromIterable([
|
|
29
|
+
{
|
|
30
|
+
url: "a",
|
|
31
|
+
title: `${query} 1`
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
url: "b",
|
|
35
|
+
title: `${query} 2`
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
url: "c",
|
|
39
|
+
title: `${query} 3`
|
|
40
|
+
}
|
|
41
|
+
]),
|
|
42
|
+
finalize: (hits) => ({ count: hits.length })
|
|
43
|
+
}),
|
|
44
|
+
streaming({
|
|
45
|
+
name: "bulk_email",
|
|
46
|
+
description: "send",
|
|
47
|
+
inputSchema: fromEffectSchema(Schema.Struct({
|
|
48
|
+
recipients: Schema.Array(Schema.String),
|
|
49
|
+
subject: Schema.String
|
|
50
|
+
})),
|
|
51
|
+
run: ({ recipients }) => Stream.fromIterable(recipients.map((_, i) => ({
|
|
52
|
+
type: "progress",
|
|
53
|
+
sent: i + 1,
|
|
54
|
+
total: recipients.length
|
|
55
|
+
}))),
|
|
56
|
+
finalize: (events) => ({
|
|
57
|
+
status: "sent",
|
|
58
|
+
delivered: events.length
|
|
59
|
+
})
|
|
60
|
+
}),
|
|
61
|
+
make({
|
|
62
|
+
name: "delete_database",
|
|
63
|
+
description: "drop",
|
|
64
|
+
inputSchema: fromEffectSchema(Schema.Struct({ name: Schema.String })),
|
|
65
|
+
run: ({ name }) => Effect.succeed({
|
|
66
|
+
status: "dropped",
|
|
67
|
+
name
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
];
|
|
71
|
+
const SENSITIVE = new Set(["bulk_email", "delete_database"]);
|
|
72
|
+
const isSensitive = (call) => SENSITIVE.has(call.name);
|
|
73
|
+
const fc = (call_id, name, args) => ({
|
|
74
|
+
type: "function_call",
|
|
75
|
+
call_id,
|
|
76
|
+
name,
|
|
77
|
+
arguments: JSON.stringify(args)
|
|
78
|
+
});
|
|
79
|
+
const calls = [
|
|
80
|
+
fc("c1", "web_search", { query: "effect" }),
|
|
81
|
+
fc("c2", "bulk_email", {
|
|
82
|
+
recipients: ["a@x", "b@x"],
|
|
83
|
+
subject: "Hi"
|
|
84
|
+
}),
|
|
85
|
+
fc("c3", "delete_database", { name: "prod" })
|
|
86
|
+
];
|
|
87
|
+
const resultsFrom = (collected) => collected.filter(isOutput).map((e) => e.result);
|
|
88
|
+
const byCallId = (results) => new Map(results.map((r) => [r.call_id, r]));
|
|
89
|
+
const eventsFromApprovalMap = (approvals) => {
|
|
90
|
+
const plan = fromApprovalMap(isSensitive, approvals)(calls);
|
|
91
|
+
return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected));
|
|
92
|
+
};
|
|
93
|
+
const eventsFromDecision = (decision) => decision._tag === "Approved" ? executeAll(allTools, [decision.call]) : Stream.succeed(outputEvent(decision.result));
|
|
94
|
+
describe("fromApprovalMap + executeAll", () => {
|
|
95
|
+
it("approval: all gated approved → tools execute, structured Values", async () => {
|
|
96
|
+
const approvals = new Map([["c2", { decision: "approve" }], ["c3", { decision: "approve" }]]);
|
|
97
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)));
|
|
98
|
+
const by = byCallId(resultsFrom(collected));
|
|
99
|
+
globalExpect(by.get("c1")).toMatchObject({
|
|
100
|
+
_tag: "Value",
|
|
101
|
+
value: { count: 3 }
|
|
102
|
+
});
|
|
103
|
+
globalExpect(by.get("c2")).toMatchObject({
|
|
104
|
+
_tag: "Value",
|
|
105
|
+
value: {
|
|
106
|
+
status: "sent",
|
|
107
|
+
delivered: 2
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
globalExpect(by.get("c3")).toMatchObject({
|
|
111
|
+
_tag: "Value",
|
|
112
|
+
value: {
|
|
113
|
+
status: "dropped",
|
|
114
|
+
name: "prod"
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
globalExpect(collected.filter(isApprovalRequested)).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
it("denial: gated denied → Failure(denied), no execution", async () => {
|
|
120
|
+
const approvals = new Map([["c2", {
|
|
121
|
+
decision: "deny",
|
|
122
|
+
reason: "spam concern"
|
|
123
|
+
}], ["c3", {
|
|
124
|
+
decision: "deny",
|
|
125
|
+
reason: "prod is sacred"
|
|
126
|
+
}]]);
|
|
127
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)));
|
|
128
|
+
globalExpect(collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email")).toHaveLength(0);
|
|
129
|
+
const by = byCallId(resultsFrom(collected));
|
|
130
|
+
globalExpect(by.get("c2")).toMatchObject({
|
|
131
|
+
_tag: "Failure",
|
|
132
|
+
kind: "denied",
|
|
133
|
+
reason: "spam concern"
|
|
134
|
+
});
|
|
135
|
+
globalExpect(by.get("c3")).toMatchObject({
|
|
136
|
+
_tag: "Failure",
|
|
137
|
+
kind: "denied",
|
|
138
|
+
reason: "prod is sacred"
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
it("cancellation: missing verdicts → Failure(cancelled)", async () => {
|
|
142
|
+
const by = byCallId(resultsFrom(await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(/* @__PURE__ */ new Map())))));
|
|
143
|
+
globalExpect(by.get("c1")).toMatchObject({
|
|
144
|
+
_tag: "Value",
|
|
145
|
+
value: { count: 3 }
|
|
146
|
+
});
|
|
147
|
+
globalExpect(by.get("c2")).toMatchObject({
|
|
148
|
+
_tag: "Failure",
|
|
149
|
+
kind: "cancelled"
|
|
150
|
+
});
|
|
151
|
+
globalExpect(by.get("c3")).toMatchObject({
|
|
152
|
+
_tag: "Failure",
|
|
153
|
+
kind: "cancelled"
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it("mixed: approve + deny + omit → all three kinds", async () => {
|
|
157
|
+
const approvals = new Map([["c2", { decision: "approve" }]]);
|
|
158
|
+
const by = byCallId(resultsFrom(await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))));
|
|
159
|
+
globalExpect(by.get("c1")).toMatchObject({
|
|
160
|
+
_tag: "Value",
|
|
161
|
+
value: { count: 3 }
|
|
162
|
+
});
|
|
163
|
+
globalExpect(by.get("c2")).toMatchObject({
|
|
164
|
+
_tag: "Value",
|
|
165
|
+
value: { status: "sent" }
|
|
166
|
+
});
|
|
167
|
+
globalExpect(by.get("c3")).toMatchObject({
|
|
168
|
+
_tag: "Failure",
|
|
169
|
+
kind: "cancelled"
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("executeAll: graceful degradation", () => {
|
|
174
|
+
it("unknown tool name → Failure(unknown_tool); other calls still execute", async () => {
|
|
175
|
+
const callsWithBogus = [
|
|
176
|
+
fc("c1", "web_search", { query: "x" }),
|
|
177
|
+
fc("c2", "does_not_exist", {}),
|
|
178
|
+
fc("c3", "delete_database", { name: "prod" })
|
|
179
|
+
];
|
|
180
|
+
const by = byCallId(resultsFrom(await Effect.runPromise(Stream.runCollect((() => {
|
|
181
|
+
const plan = fromApprovalMap(isSensitive, new Map([["c3", { decision: "approve" }]]))(callsWithBogus);
|
|
182
|
+
return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected));
|
|
183
|
+
})()))));
|
|
184
|
+
globalExpect(by.get("c1")).toMatchObject({ _tag: "Value" });
|
|
185
|
+
globalExpect(by.get("c2")).toMatchObject({
|
|
186
|
+
_tag: "Failure",
|
|
187
|
+
kind: "unknown_tool"
|
|
188
|
+
});
|
|
189
|
+
globalExpect(by.get("c3")).toMatchObject({
|
|
190
|
+
_tag: "Value",
|
|
191
|
+
value: { status: "dropped" }
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe("fromVerdictQueue + executeAll", () => {
|
|
196
|
+
it("queue-driven: approve + deny resolve correctly with ApprovalRequested events", async () => {
|
|
197
|
+
const collected = await Effect.runPromise(Effect.gen(function* () {
|
|
198
|
+
const verdicts = yield* Queue.unbounded();
|
|
199
|
+
yield* Queue.offer(verdicts, {
|
|
200
|
+
call_id: "c2",
|
|
201
|
+
decision: "approve"
|
|
202
|
+
});
|
|
203
|
+
yield* Queue.offer(verdicts, {
|
|
204
|
+
call_id: "c3",
|
|
205
|
+
decision: "deny",
|
|
206
|
+
reason: "too risky"
|
|
207
|
+
});
|
|
208
|
+
const events = Stream.unwrap(Effect.gen(function* () {
|
|
209
|
+
const { approved, decisions, announce } = yield* fromVerdictQueue(isSensitive, verdicts)(calls);
|
|
210
|
+
return Stream.merge(announce, Stream.merge(executeAll(allTools, approved), decisions.pipe(Stream.flatMap(eventsFromDecision))));
|
|
211
|
+
}));
|
|
212
|
+
return yield* Stream.runCollect(events);
|
|
213
|
+
}));
|
|
214
|
+
globalExpect(collected.filter(isApprovalRequested)).toHaveLength(2);
|
|
215
|
+
const by = byCallId(resultsFrom(collected));
|
|
216
|
+
globalExpect(by.get("c2")).toMatchObject({
|
|
217
|
+
_tag: "Value",
|
|
218
|
+
value: { status: "sent" }
|
|
219
|
+
});
|
|
220
|
+
globalExpect(by.get("c3")).toMatchObject({
|
|
221
|
+
_tag: "Failure",
|
|
222
|
+
kind: "denied",
|
|
223
|
+
reason: "too risky"
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe("findUnansweredCalls / cancelAllPending / isReconciled", () => {
|
|
228
|
+
const orphan = fc("c99", "delete_database", { name: "prod" });
|
|
229
|
+
const answered = fc("c98", "web_search", { query: "x" });
|
|
230
|
+
const answeredOutput = functionCallOutput("c98", JSON.stringify({ count: 0 }));
|
|
231
|
+
it("findUnansweredCalls returns only orphans", () => {
|
|
232
|
+
const unanswered = findUnansweredCalls([
|
|
233
|
+
userText("hi"),
|
|
234
|
+
answered,
|
|
235
|
+
orphan,
|
|
236
|
+
answeredOutput
|
|
237
|
+
]);
|
|
238
|
+
globalExpect(unanswered).toHaveLength(1);
|
|
239
|
+
globalExpect(unanswered[0].call_id).toBe("c99");
|
|
240
|
+
});
|
|
241
|
+
it("isReconciled is false when orphans exist, true otherwise", () => {
|
|
242
|
+
const stale = [userText("hi"), orphan];
|
|
243
|
+
globalExpect(isReconciled(stale)).toBe(false);
|
|
244
|
+
globalExpect(isReconciled([...stale, ...cancelAllPending(stale).map(toFunctionCallOutput)])).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
it("cancelAllPending synthesizes one Failure(cancelled) per orphan", () => {
|
|
247
|
+
const closures = cancelAllPending([
|
|
248
|
+
userText("hi"),
|
|
249
|
+
answered,
|
|
250
|
+
orphan,
|
|
251
|
+
answeredOutput
|
|
252
|
+
], "user moved on");
|
|
253
|
+
globalExpect(closures).toHaveLength(1);
|
|
254
|
+
const c = closures[0];
|
|
255
|
+
globalExpect(isFailure(c)).toBe(true);
|
|
256
|
+
globalExpect(c).toMatchObject({
|
|
257
|
+
_tag: "Failure",
|
|
258
|
+
call_id: "c99",
|
|
259
|
+
kind: "cancelled",
|
|
260
|
+
reason: "user moved on"
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
it("follow-up: map closures to FunctionCallOutput before appending new user message", () => {
|
|
264
|
+
const stale = [userText("first request"), orphan];
|
|
265
|
+
const closures = cancelAllPending(stale, "user redirected");
|
|
266
|
+
globalExpect(findUnansweredCalls([
|
|
267
|
+
...stale,
|
|
268
|
+
...closures.map(toFunctionCallOutput),
|
|
269
|
+
userText("never mind")
|
|
270
|
+
])).toHaveLength(0);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe("toFunctionCallOutput", () => {
|
|
274
|
+
it("round-trips a Value result", () => {
|
|
275
|
+
const out = toFunctionCallOutput({
|
|
276
|
+
_tag: "Value",
|
|
277
|
+
call_id: "c1",
|
|
278
|
+
tool: "web_search",
|
|
279
|
+
value: { count: 3 }
|
|
280
|
+
});
|
|
281
|
+
globalExpect(out.call_id).toBe("c1");
|
|
282
|
+
globalExpect(JSON.parse(out.output)).toEqual({ count: 3 });
|
|
283
|
+
});
|
|
284
|
+
it("round-trips a Failure result with reason", () => {
|
|
285
|
+
const out = toFunctionCallOutput({
|
|
286
|
+
_tag: "Failure",
|
|
287
|
+
call_id: "c2",
|
|
288
|
+
tool: "bulk_email",
|
|
289
|
+
kind: "denied",
|
|
290
|
+
reason: "spam concern"
|
|
291
|
+
});
|
|
292
|
+
globalExpect(JSON.parse(out.output)).toEqual({
|
|
293
|
+
kind: "denied",
|
|
294
|
+
reason: "spam concern"
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
it("round-trips a Failure result without reason (omits the field)", () => {
|
|
298
|
+
const out = toFunctionCallOutput({
|
|
299
|
+
_tag: "Failure",
|
|
300
|
+
call_id: "c3",
|
|
301
|
+
tool: "delete_database",
|
|
302
|
+
kind: "cancelled"
|
|
303
|
+
});
|
|
304
|
+
globalExpect(JSON.parse(out.output)).toEqual({ kind: "cancelled" });
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
describe("executeAll", () => {
|
|
308
|
+
it("runs all calls passed to it", async () => {
|
|
309
|
+
const collected = await Effect.runPromise(Stream.runCollect(executeAll(allTools, calls)));
|
|
310
|
+
globalExpect(collected.filter(isOutput)).toHaveLength(3);
|
|
311
|
+
globalExpect(collected.filter(isOutput).every((e) => isValue(e.result))).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
//#endregion
|
|
315
|
+
export {};
|
|
316
|
+
|
|
317
|
+
//# sourceMappingURL=Resolvers.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Resolvers.test.mjs","names":["makeTool","Items.functionCallOutput","Items.userText"],"sources":["../../src/tool/Resolvers.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 `executeAll`, 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, isValue, toFunctionCallOutput } from \"./Outcome.js\"\nimport {\n type ApprovalMapEntry,\n type ToolCallDecision,\n fromApprovalMap,\n fromVerdictQueue,\n} from \"./Resolvers.js\"\nimport { fromEffectSchema, make as makeTool, streaming } from \"./Tool.js\"\nimport { executeAll, outputEvent, outputEvents } from \"./Toolkit.js\"\nimport { type ToolEvent, isApprovalRequested, isIntermediate, 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.FunctionCall) => SENSITIVE.has(call.name)\n\nconst fc = (call_id: string, name: string, args: unknown): Items.FunctionCall => ({\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 eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {\n const plan = fromApprovalMap(isSensitive, approvals)(calls)\n return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected))\n}\n\nconst eventsFromDecision = (decision: ToolCallDecision): Stream.Stream<ToolEvent> =>\n decision._tag === \"Approved\"\n ? executeAll(allTools, [decision.call])\n : Stream.succeed(outputEvent(decision.result))\n\n// ---------------------------------------------------------------------------\n// fromApprovalMap: HTTP-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromApprovalMap + executeAll\", () => {\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: \"Value\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({\n _tag: \"Value\",\n value: { status: \"sent\", delivered: 2 },\n })\n expect(by.get(\"c3\")).toMatchObject({\n _tag: \"Value\",\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(isIntermediate).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: \"Value\", 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: \"Value\", value: { count: 3 } })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Value\", 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(\"executeAll: 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 = fromApprovalMap(\n isSensitive,\n new Map([[\"c3\", { decision: \"approve\" }]]),\n )(callsWithBogus)\n return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected))\n })(),\n ),\n )\n const by = byCallId(resultsFrom(collected))\n expect(by.get(\"c1\")).toMatchObject({ _tag: \"Value\" })\n expect(by.get(\"c2\")).toMatchObject({ _tag: \"Failure\", kind: \"unknown_tool\" })\n expect(by.get(\"c3\")).toMatchObject({ _tag: \"Value\", value: { status: \"dropped\" } })\n })\n})\n\n// ---------------------------------------------------------------------------\n// fromVerdictQueue: WebSocket-style scenarios\n// ---------------------------------------------------------------------------\n\ndescribe(\"fromVerdictQueue + executeAll\", () => {\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 fromVerdictQueue's router.\n const events = Stream.unwrap(\n Effect.gen(function* () {\n const { approved, decisions, announce } = yield* fromVerdictQueue(\n isSensitive,\n verdicts,\n )(calls)\n return Stream.merge(\n announce,\n Stream.merge(\n executeAll(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: \"Value\", 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.functionCallOutput(\"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(toFunctionCallOutput)]\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 FunctionCallOutput before appending new user message\", () => {\n const stale = [Items.userText(\"first request\"), orphan]\n const closures = cancelAllPending(stale, \"user redirected\")\n const reconciled = [\n ...stale,\n ...closures.map(toFunctionCallOutput),\n Items.userText(\"never mind\"),\n ]\n expect(findUnansweredCalls(reconciled)).toHaveLength(0)\n })\n})\n\n// ---------------------------------------------------------------------------\n// Wire conversion\n// ---------------------------------------------------------------------------\n\ndescribe(\"toFunctionCallOutput\", () => {\n it(\"round-trips a Value result\", () => {\n const r: ToolResult = {\n _tag: \"Value\",\n call_id: \"c1\",\n tool: \"web_search\",\n value: { count: 3 },\n }\n const out = toFunctionCallOutput(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 = toFunctionCallOutput(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 = toFunctionCallOutput(r)\n expect(JSON.parse(out.output)).toEqual({ kind: \"cancelled\" })\n })\n})\n\n// ---------------------------------------------------------------------------\n// executeAll\n// ---------------------------------------------------------------------------\n\ndescribe(\"executeAll\", () => {\n it(\"runs all calls passed to it\", async () => {\n const collected = await Effect.runPromise(Stream.runCollect(executeAll(allTools, calls)))\n expect(collected.filter(isOutput)).toHaveLength(3)\n expect(collected.filter(isOutput).every((e) => isValue(e.result))).toBe(true)\n })\n})\n"],"mappings":"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"}
|
package/dist/tool/Tool.d.mts
CHANGED
|
@@ -1,2 +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-
|
|
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-B8B5qVEy.mjs";
|
|
2
2
|
export { AnyKindTool, AnyPlainTool, AnyStreamingTool, StreamingTool, Tool, ToolDescriptor, ToolError, ToolInputSchema, execute, fromEffectSchema, isStreamingTool, make, streaming, toDescriptors };
|
package/dist/tool/Tool.mjs
CHANGED
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 { FunctionCall, FunctionCallOutput } from \"../domain/Items.js\"\nimport { functionCallOutput } 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\nexport
|
|
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 { FunctionCall, FunctionCallOutput } from \"../domain/Items.js\"\nimport { functionCallOutput } 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\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 `Toolkit.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.Intermediate`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 AnyKindTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>\n\nexport const isStreamingTool = <R>(t: AnyKindTool<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. Mirrors `Toolkit.toDescriptors` but accepts the union type\n * so a single list can carry both kinds.\n */\nexport const toDescriptors = <R>(\n tools: ReadonlyArray<AnyKindTool<R>>,\n): 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: FunctionCall, 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: FunctionCall,\n): Effect.Effect<FunctionCallOutput, 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 functionCallOutput(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;AA6BlE,MAAa,QACX,SACiC;AAqBnC,MAAa,aACX,UACkD;CAAE,OAAO;CAAa,GAAG;CAAM;AAMnF,MAAa,mBAAsB,MACjC,WAAW,KAAK,EAAE,UAAU;;;;;;AAO9B,MAAa,iBACX,UAEA,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,MAAoB,UAAkB,aAAqB,UAC9E,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,mBAAmB,KAAK,SAAS,KAAK,UAAU,OAAO,CAAC;EAC/D"}
|
|
@@ -1,2 +1,151 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ToolResult } from "./Outcome.mjs";
|
|
2
|
+
import { Data } from "effect";
|
|
3
|
+
import * as _$effect_Unify0 from "effect/Unify";
|
|
4
|
+
|
|
5
|
+
//#region src/tool/ToolEvent.d.ts
|
|
6
|
+
declare namespace ToolEvent_d_exports {
|
|
7
|
+
export { ToolEvent, isApprovalRequested, isIntermediate, isOutput };
|
|
8
|
+
}
|
|
9
|
+
type ToolEvent = Data.TaggedEnum<{
|
|
10
|
+
ApprovalRequested: {
|
|
11
|
+
readonly call_id: string;
|
|
12
|
+
readonly tool: string;
|
|
13
|
+
readonly arguments: string;
|
|
14
|
+
};
|
|
15
|
+
Intermediate: {
|
|
16
|
+
readonly call_id: string;
|
|
17
|
+
readonly tool: string;
|
|
18
|
+
readonly data: unknown;
|
|
19
|
+
};
|
|
20
|
+
Output: {
|
|
21
|
+
readonly result: ToolResult;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Namespace of constructors, type guards, and matchers for `ToolEvent`,
|
|
26
|
+
* provided by `Data.taggedEnum`. Use `ToolEvent.Output({ result })` to build
|
|
27
|
+
* an event, `ToolEvent.$is("Output")` for type narrowing,
|
|
28
|
+
* `ToolEvent.$match({ ApprovalRequested, Intermediate, Output })` for
|
|
29
|
+
* exhaustive pattern matching.
|
|
30
|
+
*/
|
|
31
|
+
declare const ToolEvent: {
|
|
32
|
+
readonly ApprovalRequested: Data.TaggedEnum.ConstructorFrom<{
|
|
33
|
+
readonly _tag: "ApprovalRequested";
|
|
34
|
+
readonly call_id: string;
|
|
35
|
+
readonly tool: string;
|
|
36
|
+
readonly arguments: string;
|
|
37
|
+
}, "_tag">;
|
|
38
|
+
readonly Intermediate: Data.TaggedEnum.ConstructorFrom<{
|
|
39
|
+
readonly _tag: "Intermediate";
|
|
40
|
+
readonly call_id: string;
|
|
41
|
+
readonly tool: string;
|
|
42
|
+
readonly data: unknown;
|
|
43
|
+
}, "_tag">;
|
|
44
|
+
readonly Output: Data.TaggedEnum.ConstructorFrom<{
|
|
45
|
+
readonly _tag: "Output";
|
|
46
|
+
readonly result: ToolResult;
|
|
47
|
+
}, "_tag">;
|
|
48
|
+
readonly $is: <Tag extends "ApprovalRequested" | "Intermediate" | "Output">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
49
|
+
readonly _tag: "ApprovalRequested";
|
|
50
|
+
readonly call_id: string;
|
|
51
|
+
readonly tool: string;
|
|
52
|
+
readonly arguments: string;
|
|
53
|
+
}, {
|
|
54
|
+
readonly _tag: Tag;
|
|
55
|
+
}> | Extract<{
|
|
56
|
+
readonly _tag: "Intermediate";
|
|
57
|
+
readonly call_id: string;
|
|
58
|
+
readonly tool: string;
|
|
59
|
+
readonly data: unknown;
|
|
60
|
+
}, {
|
|
61
|
+
readonly _tag: Tag;
|
|
62
|
+
}> | Extract<{
|
|
63
|
+
readonly _tag: "Output";
|
|
64
|
+
readonly result: ToolResult;
|
|
65
|
+
}, {
|
|
66
|
+
readonly _tag: Tag;
|
|
67
|
+
}>;
|
|
68
|
+
readonly $match: {
|
|
69
|
+
<Cases extends {
|
|
70
|
+
readonly ApprovalRequested: (args: {
|
|
71
|
+
readonly _tag: "ApprovalRequested";
|
|
72
|
+
readonly call_id: string;
|
|
73
|
+
readonly tool: string;
|
|
74
|
+
readonly arguments: string;
|
|
75
|
+
}) => any;
|
|
76
|
+
readonly Intermediate: (args: {
|
|
77
|
+
readonly _tag: "Intermediate";
|
|
78
|
+
readonly call_id: string;
|
|
79
|
+
readonly tool: string;
|
|
80
|
+
readonly data: unknown;
|
|
81
|
+
}) => any;
|
|
82
|
+
readonly Output: (args: {
|
|
83
|
+
readonly _tag: "Output";
|
|
84
|
+
readonly result: ToolResult;
|
|
85
|
+
}) => any;
|
|
86
|
+
}>(cases: Cases): (value: {
|
|
87
|
+
readonly _tag: "ApprovalRequested";
|
|
88
|
+
readonly call_id: string;
|
|
89
|
+
readonly tool: string;
|
|
90
|
+
readonly arguments: string;
|
|
91
|
+
} | {
|
|
92
|
+
readonly _tag: "Intermediate";
|
|
93
|
+
readonly call_id: string;
|
|
94
|
+
readonly tool: string;
|
|
95
|
+
readonly data: unknown;
|
|
96
|
+
} | {
|
|
97
|
+
readonly _tag: "Output";
|
|
98
|
+
readonly result: ToolResult;
|
|
99
|
+
}) => _$effect_Unify0.Unify<ReturnType<Cases["ApprovalRequested" | "Intermediate" | "Output"]>>;
|
|
100
|
+
<Cases extends {
|
|
101
|
+
readonly ApprovalRequested: (args: {
|
|
102
|
+
readonly _tag: "ApprovalRequested";
|
|
103
|
+
readonly call_id: string;
|
|
104
|
+
readonly tool: string;
|
|
105
|
+
readonly arguments: string;
|
|
106
|
+
}) => any;
|
|
107
|
+
readonly Intermediate: (args: {
|
|
108
|
+
readonly _tag: "Intermediate";
|
|
109
|
+
readonly call_id: string;
|
|
110
|
+
readonly tool: string;
|
|
111
|
+
readonly data: unknown;
|
|
112
|
+
}) => any;
|
|
113
|
+
readonly Output: (args: {
|
|
114
|
+
readonly _tag: "Output";
|
|
115
|
+
readonly result: ToolResult;
|
|
116
|
+
}) => any;
|
|
117
|
+
}>(value: {
|
|
118
|
+
readonly _tag: "ApprovalRequested";
|
|
119
|
+
readonly call_id: string;
|
|
120
|
+
readonly tool: string;
|
|
121
|
+
readonly arguments: string;
|
|
122
|
+
} | {
|
|
123
|
+
readonly _tag: "Intermediate";
|
|
124
|
+
readonly call_id: string;
|
|
125
|
+
readonly tool: string;
|
|
126
|
+
readonly data: unknown;
|
|
127
|
+
} | {
|
|
128
|
+
readonly _tag: "Output";
|
|
129
|
+
readonly result: ToolResult;
|
|
130
|
+
}, cases: Cases): _$effect_Unify0.Unify<ReturnType<Cases["ApprovalRequested" | "Intermediate" | "Output"]>>;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
declare const isApprovalRequested: (u: unknown) => u is {
|
|
134
|
+
readonly _tag: "ApprovalRequested";
|
|
135
|
+
readonly call_id: string;
|
|
136
|
+
readonly tool: string;
|
|
137
|
+
readonly arguments: string;
|
|
138
|
+
};
|
|
139
|
+
declare const isIntermediate: (u: unknown) => u is {
|
|
140
|
+
readonly _tag: "Intermediate";
|
|
141
|
+
readonly call_id: string;
|
|
142
|
+
readonly tool: string;
|
|
143
|
+
readonly data: unknown;
|
|
144
|
+
};
|
|
145
|
+
declare const isOutput: (u: unknown) => u is {
|
|
146
|
+
readonly _tag: "Output";
|
|
147
|
+
readonly result: ToolResult;
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
export { ToolEvent, isApprovalRequested, isIntermediate, isOutput, ToolEvent_d_exports as t };
|
|
151
|
+
//# sourceMappingURL=ToolEvent.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolEvent.d.mts","names":[],"sources":["../../src/tool/ToolEvent.ts"],"mappings":";;;;;;;;KAaY,SAAA,GAAY,IAAA,CAAK,UAAA;EAC3B,iBAAA;IAAA,SACW,OAAA;IAAA,SACA,IAAA;IAAA,SACA,SAAA;EAAA;EAEX,YAAA;IAAA,SACW,OAAA;IAAA,SACA,IAAA;IAAA,SACA,IAAA;EAAA;EAEX,MAAA;IAAA,SACW,MAAA,EAAQ,UAAA;EAAA;AAAA;;;;;AAWrB;;;cAAa,SAAA;EAAA;;;;;;;;;;;;;;qBAXQ,UAAA;EAAA;EAAA;;;;;;;;;;;;;;;;qBAAA,UAAA;EAAA;IAAA;;;;;;;;;;;;;;;;;;yBAAA,UAAA;MAAA;IAAA;;;;;;;;;;;;uBAAA,UAAA;IAAA;;;;;;;;;;;;;;;;yBAAA,UAAA;MAAA;IAAA;;;;;;;;;;;;uBAAA,UAAA;IAAA;;;cAaR,mBAAA,GAAmB,CAAA,cAAA,CAAA;EAAA;;;;;cACnB,cAAA,GAAc,CAAA,cAAA,CAAA;EAAA;;;;;cACd,QAAA,GAAQ,CAAA,cAAA,CAAA;EAAA;mBAfA,UAAA;AAAA"}
|
package/dist/tool/ToolEvent.mjs
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
+
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
|
+
import { Data } from "effect";
|
|
1
3
|
//#region src/tool/ToolEvent.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The event type emitted while handling tool calls.
|
|
6
|
+
*
|
|
7
|
+
* - ApprovalRequested : gated calls waiting for approval
|
|
8
|
+
* - Intermediate : per-element passthrough from a streaming tool's run
|
|
9
|
+
* - Output : terminal result (carries a structured ToolResult)
|
|
10
|
+
*
|
|
11
|
+
* Recipes thread `ToolEvent.Output.result` through `continueWith` and apply
|
|
12
|
+
* `toFunctionCallOutput` when appending to history.
|
|
13
|
+
*/
|
|
14
|
+
var ToolEvent_exports = /* @__PURE__ */ __exportAll({
|
|
15
|
+
ToolEvent: () => ToolEvent,
|
|
16
|
+
isApprovalRequested: () => isApprovalRequested,
|
|
17
|
+
isIntermediate: () => isIntermediate,
|
|
18
|
+
isOutput: () => isOutput
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Namespace of constructors, type guards, and matchers for `ToolEvent`,
|
|
22
|
+
* provided by `Data.taggedEnum`. Use `ToolEvent.Output({ result })` to build
|
|
23
|
+
* an event, `ToolEvent.$is("Output")` for type narrowing,
|
|
24
|
+
* `ToolEvent.$match({ ApprovalRequested, Intermediate, Output })` for
|
|
25
|
+
* exhaustive pattern matching.
|
|
26
|
+
*/
|
|
27
|
+
const ToolEvent = Data.taggedEnum();
|
|
28
|
+
const isApprovalRequested = ToolEvent.$is("ApprovalRequested");
|
|
29
|
+
const isIntermediate = ToolEvent.$is("Intermediate");
|
|
30
|
+
const isOutput = ToolEvent.$is("Output");
|
|
5
31
|
//#endregion
|
|
6
|
-
export { isApprovalRequested, isIntermediate, isOutput };
|
|
32
|
+
export { ToolEvent, isApprovalRequested, isIntermediate, isOutput, ToolEvent_exports as t };
|
|
7
33
|
|
|
8
34
|
//# sourceMappingURL=ToolEvent.mjs.map
|