@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
package/src/tool/HistoryCheck.ts
CHANGED
|
@@ -23,9 +23,7 @@ import { type ToolResult, cancelled } from "./Outcome.js"
|
|
|
23
23
|
* `function_call_output` later in `history` (correlated by `call_id`).
|
|
24
24
|
* Empty result = history is provider-submittable from this invariant.
|
|
25
25
|
*/
|
|
26
|
-
export const findUnansweredCalls = (
|
|
27
|
-
history: ReadonlyArray<Item>,
|
|
28
|
-
): ReadonlyArray<FunctionCall> => {
|
|
26
|
+
export const findUnansweredCalls = (history: ReadonlyArray<Item>): ReadonlyArray<FunctionCall> => {
|
|
29
27
|
const answered = new Set(history.filter(isFunctionCallOutput).map((o) => o.call_id))
|
|
30
28
|
return history.filter(isFunctionCall).filter((c) => !answered.has(c.call_id))
|
|
31
29
|
}
|
|
@@ -45,5 +43,4 @@ export const isReconciled = (history: ReadonlyArray<Item>): boolean =>
|
|
|
45
43
|
export const cancelAllPending = (
|
|
46
44
|
history: ReadonlyArray<Item>,
|
|
47
45
|
reason?: string,
|
|
48
|
-
): ReadonlyArray<ToolResult> =>
|
|
49
|
-
findUnansweredCalls(history).map((call) => cancelled(call, reason))
|
|
46
|
+
): ReadonlyArray<ToolResult> => findUnansweredCalls(history).map((call) => cancelled(call, reason))
|
package/src/tool/Outcome.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and `unknown` would invite non-serializable values (Date, Map, BigInt,
|
|
12
12
|
* fn). Recipes that want structured detail JSON.stringify themselves.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { Data } from "effect"
|
|
15
15
|
import type { FunctionCall, FunctionCallOutput } from "../domain/Items.js"
|
|
16
16
|
import { functionCallOutput } from "../domain/Items.js"
|
|
17
17
|
|
|
@@ -19,42 +19,44 @@ import { functionCallOutput } from "../domain/Items.js"
|
|
|
19
19
|
// ToolResult
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
22
|
-
export type ToolResult =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
22
|
+
export type ToolResult = Data.TaggedEnum<{
|
|
23
|
+
Value: {
|
|
24
|
+
readonly call_id: string
|
|
25
|
+
readonly tool: string
|
|
26
|
+
readonly value: unknown
|
|
27
|
+
}
|
|
28
|
+
Failure: {
|
|
29
|
+
readonly call_id: string
|
|
30
|
+
readonly tool: string
|
|
31
|
+
readonly kind: string
|
|
32
|
+
readonly reason?: string
|
|
33
|
+
}
|
|
34
|
+
}>
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Namespace of constructors, type guards, and matchers for `ToolResult`,
|
|
38
|
+
* provided by `Data.taggedEnum`. Use `ToolResult.$is("Value")` for type
|
|
39
|
+
* narrowing and `ToolResult.$match({ Value, Failure })` for exhaustive
|
|
40
|
+
* pattern matching. Synthetic-result helpers (`denied`, `cancelled`,
|
|
41
|
+
* `executionError`, `rejected`) below are kinder constructors than the
|
|
42
|
+
* raw `ToolResult.Failure(...)`.
|
|
43
|
+
*/
|
|
44
|
+
export const ToolResult = Data.taggedEnum<ToolResult>()
|
|
39
45
|
|
|
40
|
-
export const
|
|
41
|
-
|
|
46
|
+
export const isValue = ToolResult.$is("Value")
|
|
47
|
+
export const isFailure = ToolResult.$is("Failure")
|
|
42
48
|
|
|
43
49
|
// Synthesizers. `denied` and `cancelled` are operationally distinct;
|
|
44
50
|
// anything else is just a recipe-chosen `kind` via `rejected`.
|
|
45
51
|
// ---------------------------------------------------------------------------
|
|
46
52
|
|
|
47
|
-
export const rejected = (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
tool: call.name,
|
|
55
|
-
kind,
|
|
56
|
-
...(reason !== undefined ? { reason } : {}),
|
|
57
|
-
})
|
|
53
|
+
export const rejected = (call: FunctionCall, kind: string, reason?: string): ToolResult =>
|
|
54
|
+
ToolResult.Failure({
|
|
55
|
+
call_id: call.call_id,
|
|
56
|
+
tool: call.name,
|
|
57
|
+
kind,
|
|
58
|
+
...(reason !== undefined ? { reason } : {}),
|
|
59
|
+
})
|
|
58
60
|
|
|
59
61
|
/** Explicit user/policy rejection. */
|
|
60
62
|
export const denied = (call: FunctionCall, reason?: string): ToolResult =>
|
|
@@ -73,15 +75,13 @@ export const executionError = (call: FunctionCall, reason: string): ToolResult =
|
|
|
73
75
|
// ---------------------------------------------------------------------------
|
|
74
76
|
|
|
75
77
|
export const toFunctionCallOutput = (r: ToolResult): FunctionCallOutput =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
ToolResult.$match(r, {
|
|
79
|
+
Value: (v) => functionCallOutput(v.call_id, JSON.stringify(v.value)),
|
|
80
|
+
Failure: (f) =>
|
|
79
81
|
functionCallOutput(
|
|
80
82
|
f.call_id,
|
|
81
83
|
JSON.stringify(
|
|
82
84
|
f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },
|
|
83
85
|
),
|
|
84
86
|
),
|
|
85
|
-
|
|
86
|
-
Match.exhaustive,
|
|
87
|
-
)
|
|
87
|
+
})
|
|
@@ -14,12 +14,7 @@ import { Effect, Queue, Schema, Stream } from "effect"
|
|
|
14
14
|
import { describe, expect, it } from "vitest"
|
|
15
15
|
import * as Items from "../domain/Items.js"
|
|
16
16
|
import { findUnansweredCalls, cancelAllPending, isReconciled } from "./HistoryCheck.js"
|
|
17
|
-
import {
|
|
18
|
-
type ToolResult,
|
|
19
|
-
isFailure,
|
|
20
|
-
isValue,
|
|
21
|
-
toFunctionCallOutput,
|
|
22
|
-
} from "./Outcome.js"
|
|
17
|
+
import { type ToolResult, isFailure, isValue, toFunctionCallOutput } from "./Outcome.js"
|
|
23
18
|
import {
|
|
24
19
|
type ApprovalMapEntry,
|
|
25
20
|
type ToolCallDecision,
|
|
@@ -28,12 +23,7 @@ import {
|
|
|
28
23
|
} from "./Resolvers.js"
|
|
29
24
|
import { fromEffectSchema, make as makeTool, streaming } from "./Tool.js"
|
|
30
25
|
import { executeAll, outputEvent, outputEvents } from "./Toolkit.js"
|
|
31
|
-
import {
|
|
32
|
-
type ToolEvent,
|
|
33
|
-
isApprovalRequested,
|
|
34
|
-
isIntermediate,
|
|
35
|
-
isOutput,
|
|
36
|
-
} from "./ToolEvent.js"
|
|
26
|
+
import { type ToolEvent, isApprovalRequested, isIntermediate, isOutput } from "./ToolEvent.js"
|
|
37
27
|
|
|
38
28
|
// ---------------------------------------------------------------------------
|
|
39
29
|
// Three demo tools covering the matrix:
|
|
@@ -96,12 +86,10 @@ const calls = [
|
|
|
96
86
|
fc("c3", "delete_database", { name: "prod" }),
|
|
97
87
|
]
|
|
98
88
|
|
|
99
|
-
const resultsFrom = (
|
|
100
|
-
collected
|
|
101
|
-
): ReadonlyArray<ToolResult> => collected.filter(isOutput).map((e) => e.result)
|
|
89
|
+
const resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolResult> =>
|
|
90
|
+
collected.filter(isOutput).map((e) => e.result)
|
|
102
91
|
|
|
103
|
-
const byCallId = (results: ReadonlyArray<ToolResult>) =>
|
|
104
|
-
new Map(results.map((r) => [r.call_id, r]))
|
|
92
|
+
const byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))
|
|
105
93
|
|
|
106
94
|
const eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {
|
|
107
95
|
const plan = fromApprovalMap(isSensitive, approvals)(calls)
|
|
@@ -123,9 +111,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
123
111
|
["c2", { decision: "approve" }],
|
|
124
112
|
["c3", { decision: "approve" }],
|
|
125
113
|
])
|
|
126
|
-
const collected = await Effect.runPromise(
|
|
127
|
-
Stream.runCollect(eventsFromApprovalMap(approvals)),
|
|
128
|
-
)
|
|
114
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
129
115
|
const by = byCallId(resultsFrom(collected))
|
|
130
116
|
expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
|
|
131
117
|
expect(by.get("c2")).toMatchObject({
|
|
@@ -146,14 +132,10 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
146
132
|
["c2", { decision: "deny", reason: "spam concern" }],
|
|
147
133
|
["c3", { decision: "deny", reason: "prod is sacred" }],
|
|
148
134
|
])
|
|
149
|
-
const collected = await Effect.runPromise(
|
|
150
|
-
Stream.runCollect(eventsFromApprovalMap(approvals)),
|
|
151
|
-
)
|
|
135
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
152
136
|
|
|
153
137
|
// bulk_email never ran.
|
|
154
|
-
expect(
|
|
155
|
-
collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email"),
|
|
156
|
-
).toHaveLength(0)
|
|
138
|
+
expect(collected.filter(isIntermediate).filter((e) => e.tool === "bulk_email")).toHaveLength(0)
|
|
157
139
|
|
|
158
140
|
const by = byCallId(resultsFrom(collected))
|
|
159
141
|
expect(by.get("c2")).toMatchObject({
|
|
@@ -169,9 +151,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
169
151
|
})
|
|
170
152
|
|
|
171
153
|
it("cancellation: missing verdicts → Failure(cancelled)", async () => {
|
|
172
|
-
const collected = await Effect.runPromise(
|
|
173
|
-
Stream.runCollect(eventsFromApprovalMap(new Map())),
|
|
174
|
-
)
|
|
154
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(new Map())))
|
|
175
155
|
const by = byCallId(resultsFrom(collected))
|
|
176
156
|
expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
|
|
177
157
|
expect(by.get("c2")).toMatchObject({ _tag: "Failure", kind: "cancelled" })
|
|
@@ -183,9 +163,7 @@ describe("fromApprovalMap + executeAll", () => {
|
|
|
183
163
|
["c2", { decision: "approve" }],
|
|
184
164
|
// c3 omitted → cancelled
|
|
185
165
|
])
|
|
186
|
-
const collected = await Effect.runPromise(
|
|
187
|
-
Stream.runCollect(eventsFromApprovalMap(approvals)),
|
|
188
|
-
)
|
|
166
|
+
const collected = await Effect.runPromise(Stream.runCollect(eventsFromApprovalMap(approvals)))
|
|
189
167
|
const by = byCallId(resultsFrom(collected))
|
|
190
168
|
expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
|
|
191
169
|
expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
|
|
@@ -370,9 +348,7 @@ describe("toFunctionCallOutput", () => {
|
|
|
370
348
|
|
|
371
349
|
describe("executeAll", () => {
|
|
372
350
|
it("runs all calls passed to it", async () => {
|
|
373
|
-
const collected = await Effect.runPromise(
|
|
374
|
-
Stream.runCollect(executeAll(allTools, calls)),
|
|
375
|
-
)
|
|
351
|
+
const collected = await Effect.runPromise(Stream.runCollect(executeAll(allTools, calls)))
|
|
376
352
|
expect(collected.filter(isOutput)).toHaveLength(3)
|
|
377
353
|
expect(collected.filter(isOutput).every((e) => isValue(e.result))).toBe(true)
|
|
378
354
|
})
|
package/src/tool/Resolvers.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { FunctionCall } from "../domain/Items.js"
|
|
|
10
10
|
import { type ToolResult, cancelled, denied } from "./Outcome.js"
|
|
11
11
|
import type { ToolEvent } from "./ToolEvent.js"
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export type ToolCallPlan = {
|
|
14
14
|
readonly approved: ReadonlyArray<FunctionCall>
|
|
15
15
|
readonly rejected: ReadonlyArray<ToolResult>
|
|
16
16
|
}
|
|
@@ -29,9 +29,7 @@ export const reject = (result: ToolResult): ToolCallDecision => ({
|
|
|
29
29
|
result,
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
export const splitToolCallDecisions = (
|
|
33
|
-
decisions: ReadonlyArray<ToolCallDecision>,
|
|
34
|
-
): ToolCallPlan =>
|
|
32
|
+
export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision>): ToolCallPlan =>
|
|
35
33
|
decisions.reduce<ToolCallPlan>(
|
|
36
34
|
(acc, decision) =>
|
|
37
35
|
decision._tag === "Approved"
|
|
@@ -51,7 +49,7 @@ export const approvalRequested = (call: FunctionCall): ToolEvent => ({
|
|
|
51
49
|
// Verdict queue (WebSocket-style transport).
|
|
52
50
|
// ---------------------------------------------------------------------------
|
|
53
51
|
|
|
54
|
-
export
|
|
52
|
+
export type Verdict = {
|
|
55
53
|
readonly call_id: string
|
|
56
54
|
readonly decision: "approve" | "deny"
|
|
57
55
|
readonly reason?: string
|
|
@@ -63,10 +61,7 @@ export interface Verdict {
|
|
|
63
61
|
* one `ToolCallDecision` when their matching verdict arrives.
|
|
64
62
|
*/
|
|
65
63
|
export const fromVerdictQueue =
|
|
66
|
-
(
|
|
67
|
-
predicate: (call: FunctionCall) => boolean,
|
|
68
|
-
verdicts: Queue.Dequeue<Verdict>,
|
|
69
|
-
) =>
|
|
64
|
+
(predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) =>
|
|
70
65
|
(
|
|
71
66
|
calls: ReadonlyArray<FunctionCall>,
|
|
72
67
|
): Effect.Effect<
|
|
@@ -131,10 +126,7 @@ export type ApprovalMapEntry =
|
|
|
131
126
|
| { readonly decision: "deny"; readonly reason?: string }
|
|
132
127
|
|
|
133
128
|
export const fromApprovalMap =
|
|
134
|
-
(
|
|
135
|
-
predicate: (call: FunctionCall) => boolean,
|
|
136
|
-
approvals: ReadonlyMap<string, ApprovalMapEntry>,
|
|
137
|
-
) =>
|
|
129
|
+
(predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>
|
|
138
130
|
(calls: ReadonlyArray<FunctionCall>): ToolCallPlan =>
|
|
139
131
|
splitToolCallDecisions(
|
|
140
132
|
calls.map((call) => {
|
|
@@ -144,4 +136,3 @@ export const fromApprovalMap =
|
|
|
144
136
|
return v.decision === "approve" ? approve(call) : reject(denied(call, v.reason))
|
|
145
137
|
}),
|
|
146
138
|
)
|
|
147
|
-
|
package/src/tool/Tool.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(
|
|
|
36
36
|
Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)) as unknown as S &
|
|
37
37
|
ToolInputSchema<S["Type"]>
|
|
38
38
|
|
|
39
|
-
export
|
|
39
|
+
export type Tool<Name extends string, Input, Output, R = never> = {
|
|
40
40
|
readonly name: Name
|
|
41
41
|
readonly description: string
|
|
42
42
|
readonly inputSchema: ToolInputSchema<Input>
|
|
@@ -55,7 +55,7 @@ export interface Tool<Name extends string, Input, Output, R = never> {
|
|
|
55
55
|
* to its own wire field (OpenAI → `parameters`, Anthropic →
|
|
56
56
|
* `input_schema`). Built from a `Tool` by `Toolkit.toDescriptors`.
|
|
57
57
|
*/
|
|
58
|
-
export
|
|
58
|
+
export type ToolDescriptor = {
|
|
59
59
|
readonly name: string
|
|
60
60
|
readonly description: string
|
|
61
61
|
readonly inputSchema: Record<string, unknown>
|
|
@@ -75,7 +75,7 @@ export const make = <Name extends string, Input, Output, R = never>(
|
|
|
75
75
|
// `Output`. Sub-agents, slow downloads with progress, recipe streamers.
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
-
export
|
|
78
|
+
export type StreamingTool<Name extends string, Input, Event, Output, R = never> = {
|
|
79
79
|
readonly _kind: "streaming"
|
|
80
80
|
readonly name: Name
|
|
81
81
|
readonly description: string
|
|
@@ -89,11 +89,11 @@ export const streaming = <Name extends string, Input, Event, Output, R = never>(
|
|
|
89
89
|
spec: Omit<StreamingTool<Name, Input, Event, Output, R>, "_kind">,
|
|
90
90
|
): StreamingTool<Name, Input, Event, Output, R> => ({ _kind: "streaming", ...spec })
|
|
91
91
|
|
|
92
|
-
export type AnyStreamingTool = StreamingTool<string, any, any, any,
|
|
93
|
-
export type AnyPlainTool = Tool<string, any, any,
|
|
94
|
-
export type AnyKindTool = AnyStreamingTool | AnyPlainTool
|
|
92
|
+
export type AnyStreamingTool<R = any> = StreamingTool<string, any, any, any, R>
|
|
93
|
+
export type AnyPlainTool<R = any> = Tool<string, any, any, R>
|
|
94
|
+
export type AnyKindTool<R = any> = AnyStreamingTool<R> | AnyPlainTool<R>
|
|
95
95
|
|
|
96
|
-
export const isStreamingTool = (t: AnyKindTool): t is AnyStreamingTool =>
|
|
96
|
+
export const isStreamingTool = <R>(t: AnyKindTool<R>): t is AnyStreamingTool<R> =>
|
|
97
97
|
"_kind" in t && t._kind === "streaming"
|
|
98
98
|
|
|
99
99
|
/**
|
|
@@ -101,8 +101,8 @@ export const isStreamingTool = (t: AnyKindTool): t is AnyStreamingTool =>
|
|
|
101
101
|
* descriptors. Mirrors `Toolkit.toDescriptors` but accepts the union type
|
|
102
102
|
* so a single list can carry both kinds.
|
|
103
103
|
*/
|
|
104
|
-
export const toDescriptors = (
|
|
105
|
-
tools: ReadonlyArray<AnyKindTool
|
|
104
|
+
export const toDescriptors = <R>(
|
|
105
|
+
tools: ReadonlyArray<AnyKindTool<R>>,
|
|
106
106
|
): ReadonlyArray<ToolDescriptor> =>
|
|
107
107
|
tools.map((tool) => {
|
|
108
108
|
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
|
package/src/tool/ToolEvent.ts
CHANGED
|
@@ -5,33 +5,37 @@
|
|
|
5
5
|
* - Intermediate : per-element passthrough from a streaming tool's run
|
|
6
6
|
* - Output : terminal result (carries a structured ToolResult)
|
|
7
7
|
*
|
|
8
|
-
* Recipes thread `ToolEvent.Output.result` through `
|
|
8
|
+
* Recipes thread `ToolEvent.Output.result` through `continueWith` and apply
|
|
9
9
|
* `toFunctionCallOutput` when appending to history.
|
|
10
10
|
*/
|
|
11
|
+
import { Data } from "effect"
|
|
11
12
|
import type { ToolResult } from "./Outcome.js"
|
|
12
13
|
|
|
13
|
-
export type ToolEvent =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
export type ToolEvent = Data.TaggedEnum<{
|
|
15
|
+
ApprovalRequested: {
|
|
16
|
+
readonly call_id: string
|
|
17
|
+
readonly tool: string
|
|
18
|
+
readonly arguments: string
|
|
19
|
+
}
|
|
20
|
+
Intermediate: {
|
|
21
|
+
readonly call_id: string
|
|
22
|
+
readonly tool: string
|
|
23
|
+
readonly data: unknown
|
|
24
|
+
}
|
|
25
|
+
Output: {
|
|
26
|
+
readonly result: ToolResult
|
|
27
|
+
}
|
|
28
|
+
}>
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Namespace of constructors, type guards, and matchers for `ToolEvent`,
|
|
32
|
+
* provided by `Data.taggedEnum`. Use `ToolEvent.Output({ result })` to build
|
|
33
|
+
* an event, `ToolEvent.$is("Output")` for type narrowing,
|
|
34
|
+
* `ToolEvent.$match({ ApprovalRequested, Intermediate, Output })` for
|
|
35
|
+
* exhaustive pattern matching.
|
|
36
|
+
*/
|
|
37
|
+
export const ToolEvent = Data.taggedEnum<ToolEvent>()
|
|
35
38
|
|
|
36
|
-
export const
|
|
37
|
-
|
|
39
|
+
export const isApprovalRequested = ToolEvent.$is("ApprovalRequested")
|
|
40
|
+
export const isIntermediate = ToolEvent.$is("Intermediate")
|
|
41
|
+
export const isOutput = ToolEvent.$is("Output")
|
package/src/tool/Toolkit.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect"
|
|
2
|
-
import { describe, expect, it } from "vitest"
|
|
1
|
+
import { Context, Effect, Layer, Schema, Stream } from "effect"
|
|
2
|
+
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
3
|
+
import type { FunctionCall } from "../domain/Items.js"
|
|
4
|
+
import { isOutput } from "./ToolEvent.js"
|
|
5
|
+
import { isValue } from "./Outcome.js"
|
|
3
6
|
import * as Tool from "./Tool.js"
|
|
4
7
|
import * as Toolkit from "./Toolkit.js"
|
|
5
8
|
|
|
@@ -43,3 +46,95 @@ describe("Toolkit.toDescriptors", () => {
|
|
|
43
46
|
expect(l).not.toHaveProperty("strict")
|
|
44
47
|
})
|
|
45
48
|
})
|
|
49
|
+
|
|
50
|
+
describe("Toolkit.executeAll - tools with R requirements", () => {
|
|
51
|
+
// Two distinct services, modelling the "typed per-tool context" use case
|
|
52
|
+
// (cf. AI SDK 7's `toolsContext`). In Effect each tool declares its R, the
|
|
53
|
+
// compiler enforces it, and `executeAll` surfaces the union for the caller
|
|
54
|
+
// to provide via Layer.
|
|
55
|
+
type WeatherApiKeyShape = { readonly key: string }
|
|
56
|
+
class WeatherApiKey extends Context.Service<WeatherApiKey, WeatherApiKeyShape>()(
|
|
57
|
+
"test/WeatherApiKey",
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
type GeoApiKeyShape = { readonly key: string }
|
|
61
|
+
class GeoApiKey extends Context.Service<GeoApiKey, GeoApiKeyShape>()("test/GeoApiKey") {}
|
|
62
|
+
|
|
63
|
+
const Empty = Schema.Struct({})
|
|
64
|
+
|
|
65
|
+
const getWeather = Tool.make({
|
|
66
|
+
name: "get_weather",
|
|
67
|
+
description: "",
|
|
68
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
69
|
+
run: () =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const { key } = yield* WeatherApiKey
|
|
72
|
+
return { source: "weather", key }
|
|
73
|
+
}),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const getCoords = Tool.make({
|
|
77
|
+
name: "get_coords",
|
|
78
|
+
description: "",
|
|
79
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
80
|
+
run: () =>
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
const { key } = yield* GeoApiKey
|
|
83
|
+
return { source: "geo", key }
|
|
84
|
+
}),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const call = (name: string, id: string): FunctionCall => ({
|
|
88
|
+
type: "function_call",
|
|
89
|
+
call_id: id,
|
|
90
|
+
name,
|
|
91
|
+
arguments: "{}",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("propagates each tool's R into the resulting Stream's requirements", () => {
|
|
95
|
+
const stream = Toolkit.executeAll([getWeather, getCoords], [])
|
|
96
|
+
expectTypeOf(stream).toEqualTypeOf<
|
|
97
|
+
Stream.Stream<import("./ToolEvent.js").ToolEvent, never, WeatherApiKey | GeoApiKey>
|
|
98
|
+
>()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("runs each tool with its own service injected", async () => {
|
|
102
|
+
const layer = Layer.mergeAll(
|
|
103
|
+
Layer.succeed(WeatherApiKey, { key: "weather-123" }),
|
|
104
|
+
Layer.succeed(GeoApiKey, { key: "geo-456" }),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const program = Toolkit.executeAll(
|
|
108
|
+
[getWeather, getCoords],
|
|
109
|
+
[call("get_weather", "c1"), call("get_coords", "c2")],
|
|
110
|
+
).pipe(Stream.runCollect, Effect.provide(layer))
|
|
111
|
+
|
|
112
|
+
const events = await Effect.runPromise(program)
|
|
113
|
+
const outputs = Array.from(events).filter(isOutput)
|
|
114
|
+
const byCall = new Map(outputs.map((e) => [e.result.call_id, e.result]))
|
|
115
|
+
|
|
116
|
+
const w = byCall.get("c1")
|
|
117
|
+
const g = byCall.get("c2")
|
|
118
|
+
expect(w !== undefined && isValue(w) && w.value).toEqual({
|
|
119
|
+
source: "weather",
|
|
120
|
+
key: "weather-123",
|
|
121
|
+
})
|
|
122
|
+
expect(g !== undefined && isValue(g) && g.value).toEqual({
|
|
123
|
+
source: "geo",
|
|
124
|
+
key: "geo-456",
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("with no service-needing tools, R is never", () => {
|
|
129
|
+
const plain = Tool.make({
|
|
130
|
+
name: "plain",
|
|
131
|
+
description: "",
|
|
132
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
133
|
+
run: () => Effect.succeed(0),
|
|
134
|
+
})
|
|
135
|
+
const stream = Toolkit.executeAll([plain], [])
|
|
136
|
+
expectTypeOf(stream).toEqualTypeOf<
|
|
137
|
+
Stream.Stream<import("./ToolEvent.js").ToolEvent, never, never>
|
|
138
|
+
>()
|
|
139
|
+
})
|
|
140
|
+
})
|
package/src/tool/Toolkit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Array as Arr, Effect, Ref, Stream } from "effect"
|
|
1
|
+
import { Array as Arr, Effect, Function, Ref, Stream } from "effect"
|
|
2
2
|
import * as Loop from "../loop/Loop.js"
|
|
3
3
|
import type { FunctionCall } from "../domain/Items.js"
|
|
4
4
|
import {
|
|
@@ -6,14 +6,11 @@ import {
|
|
|
6
6
|
type AnyPlainTool,
|
|
7
7
|
type AnyStreamingTool,
|
|
8
8
|
isStreamingTool,
|
|
9
|
+
type StreamingTool,
|
|
9
10
|
type Tool,
|
|
10
11
|
type ToolDescriptor,
|
|
11
12
|
} from "./Tool.js"
|
|
12
|
-
import {
|
|
13
|
-
type ToolResult,
|
|
14
|
-
executionError,
|
|
15
|
-
rejected,
|
|
16
|
-
} from "./Outcome.js"
|
|
13
|
+
import { type ToolResult, executionError, rejected } from "./Outcome.js"
|
|
17
14
|
import type { ToolEvent } from "./ToolEvent.js"
|
|
18
15
|
import { isOutput } from "./ToolEvent.js"
|
|
19
16
|
|
|
@@ -26,6 +23,18 @@ export type Toolkit<Tools extends ReadonlyArray<AnyTool>> = {
|
|
|
26
23
|
export type ToolsR<Tools extends ReadonlyArray<AnyTool>> =
|
|
27
24
|
Tools[number] extends Tool<any, any, any, infer R> ? R : never
|
|
28
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Union of every tool's `R` requirements in a mixed plain + streaming array.
|
|
28
|
+
* Used by `executeAll` to surface the services tools need at the recipe
|
|
29
|
+
* level, so the loop's stream type carries them through to `Effect.provide`.
|
|
30
|
+
*/
|
|
31
|
+
export type ToolKindR<Tools extends ReadonlyArray<AnyKindTool<any>>> =
|
|
32
|
+
Tools[number] extends StreamingTool<any, any, any, any, infer R>
|
|
33
|
+
? R
|
|
34
|
+
: Tools[number] extends Tool<any, any, any, infer R>
|
|
35
|
+
? R
|
|
36
|
+
: never
|
|
37
|
+
|
|
29
38
|
export const make = <const Tools extends ReadonlyArray<AnyTool>>(tools: Tools): Toolkit<Tools> => ({
|
|
30
39
|
tools,
|
|
31
40
|
})
|
|
@@ -53,16 +62,16 @@ export const toDescriptors = <Tools extends ReadonlyArray<AnyTool>>(
|
|
|
53
62
|
// only the calls they have already decided should run.
|
|
54
63
|
// ---------------------------------------------------------------------------
|
|
55
64
|
|
|
56
|
-
export
|
|
65
|
+
export type ExecuteOptions = {
|
|
57
66
|
readonly concurrency?: number | "unbounded"
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/** Execute every provided call. Approval/rejection policy belongs upstream. */
|
|
61
|
-
export const executeAll = (
|
|
62
|
-
tools:
|
|
70
|
+
export const executeAll = <Tools extends ReadonlyArray<AnyKindTool<any>>>(
|
|
71
|
+
tools: Tools,
|
|
63
72
|
calls: ReadonlyArray<FunctionCall>,
|
|
64
73
|
options?: ExecuteOptions,
|
|
65
|
-
): Stream.Stream<ToolEvent
|
|
74
|
+
): Stream.Stream<ToolEvent, never, ToolKindR<Tools>> =>
|
|
66
75
|
Stream.fromIterable(calls).pipe(
|
|
67
76
|
Stream.flatMap((call) => runOne(tools, call), {
|
|
68
77
|
concurrency: options?.concurrency ?? "unbounded",
|
|
@@ -71,9 +80,8 @@ export const executeAll = (
|
|
|
71
80
|
|
|
72
81
|
export const outputEvent = (result: ToolResult): ToolEvent => ({ _tag: "Output", result })
|
|
73
82
|
|
|
74
|
-
export const outputEvents = (
|
|
75
|
-
results
|
|
76
|
-
): Stream.Stream<ToolEvent> => Stream.fromIterable(results.map(outputEvent))
|
|
83
|
+
export const outputEvents = (results: ReadonlyArray<ToolResult>): Stream.Stream<ToolEvent> =>
|
|
84
|
+
Stream.fromIterable(results.map(outputEvent))
|
|
77
85
|
|
|
78
86
|
const valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResult => ({
|
|
79
87
|
_tag: "Value",
|
|
@@ -82,10 +90,10 @@ const valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResu
|
|
|
82
90
|
value,
|
|
83
91
|
})
|
|
84
92
|
|
|
85
|
-
const runOne = (
|
|
86
|
-
tools: ReadonlyArray<AnyKindTool
|
|
93
|
+
const runOne = <R>(
|
|
94
|
+
tools: ReadonlyArray<AnyKindTool<R>>,
|
|
87
95
|
call: FunctionCall,
|
|
88
|
-
): Stream.Stream<ToolEvent> => {
|
|
96
|
+
): Stream.Stream<ToolEvent, never, R> => {
|
|
89
97
|
const tool = tools.find((t) => t.name === call.name)
|
|
90
98
|
if (tool === undefined) {
|
|
91
99
|
// Graceful: emit a synthetic Failure so OTHER calls in this turn
|
|
@@ -99,10 +107,10 @@ const runOne = (
|
|
|
99
107
|
return runPlain(tool, call)
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
const runPlain = (
|
|
103
|
-
tool: AnyPlainTool
|
|
110
|
+
const runPlain = <R>(
|
|
111
|
+
tool: AnyPlainTool<R>,
|
|
104
112
|
call: FunctionCall,
|
|
105
|
-
): Stream.Stream<ToolEvent> =>
|
|
113
|
+
): Stream.Stream<ToolEvent, never, R> =>
|
|
106
114
|
Stream.fromEffect(
|
|
107
115
|
Effect.gen(function* () {
|
|
108
116
|
const parsed = yield* Effect.try({
|
|
@@ -124,10 +132,10 @@ const runPlain = (
|
|
|
124
132
|
),
|
|
125
133
|
)
|
|
126
134
|
|
|
127
|
-
const runStreaming = (
|
|
128
|
-
tool: AnyStreamingTool
|
|
135
|
+
const runStreaming = <R>(
|
|
136
|
+
tool: AnyStreamingTool<R>,
|
|
129
137
|
call: FunctionCall,
|
|
130
|
-
): Stream.Stream<ToolEvent> =>
|
|
138
|
+
): Stream.Stream<ToolEvent, never, R> =>
|
|
131
139
|
Stream.unwrap(
|
|
132
140
|
Effect.gen(function* () {
|
|
133
141
|
const parsed = yield* Effect.try({
|
|
@@ -184,19 +192,35 @@ const runStreaming = (
|
|
|
184
192
|
)
|
|
185
193
|
|
|
186
194
|
// ---------------------------------------------------------------------------
|
|
187
|
-
// `
|
|
195
|
+
// `continueWith` - bridge from a `Stream<ToolEvent>` to the loop's emit
|
|
188
196
|
// shape. Drains the stream to the consumer in real-time, taps every
|
|
189
197
|
// `Output` into an internal Ref, and at end-of-stream emits
|
|
190
198
|
// `Loop.next(build(results))`. Recipe never sees the Ref.
|
|
199
|
+
//
|
|
200
|
+
// Dual: data-first `continueWith(stream, build)` and data-last
|
|
201
|
+
// `stream.pipe(continueWith(build))` both work.
|
|
191
202
|
// ---------------------------------------------------------------------------
|
|
192
203
|
|
|
193
|
-
export const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
):
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
build,
|
|
202
|
-
)
|
|
204
|
+
export const continueWith: {
|
|
205
|
+
<S>(
|
|
206
|
+
build: (results: ReadonlyArray<ToolResult>) => S,
|
|
207
|
+
): <R>(
|
|
208
|
+
stream: Stream.Stream<ToolEvent, never, R>,
|
|
209
|
+
) => Stream.Stream<Loop.Event<ToolEvent, S>, never, R>
|
|
210
|
+
<S, R>(
|
|
211
|
+
stream: Stream.Stream<ToolEvent, never, R>,
|
|
212
|
+
build: (results: ReadonlyArray<ToolResult>) => S,
|
|
213
|
+
): Stream.Stream<Loop.Event<ToolEvent, S>, never, R>
|
|
214
|
+
} = Function.dual(
|
|
215
|
+
2,
|
|
216
|
+
<S, R>(
|
|
217
|
+
stream: Stream.Stream<ToolEvent, never, R>,
|
|
218
|
+
build: (results: ReadonlyArray<ToolResult>) => S,
|
|
219
|
+
): Stream.Stream<Loop.Event<ToolEvent, S>, never, R> =>
|
|
220
|
+
Loop.nextAfterFold(
|
|
221
|
+
stream,
|
|
222
|
+
[] as ReadonlyArray<ToolResult>,
|
|
223
|
+
(acc, e) => (isOutput(e) ? Arr.append(acc, e.result) : acc),
|
|
224
|
+
build,
|
|
225
|
+
),
|
|
226
|
+
)
|