@effect-uai/core 0.1.0 → 0.2.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/{Outcome-C2JYknCu.d.mts → Outcome-GiaNvt7i.d.mts} +2 -10
- package/dist/Outcome-GiaNvt7i.d.mts.map +1 -0
- package/dist/{ToolEvent-B2N10hr3.d.mts → ToolEvent-wTMgb2GO.d.mts} +2 -2
- package/dist/{ToolEvent-B2N10hr3.d.mts.map → ToolEvent-wTMgb2GO.d.mts.map} +1 -1
- package/dist/{Turn-rlTfuHaQ.d.mts → Turn-Bi83du4I.d.mts} +8 -19
- package/dist/{Turn-rlTfuHaQ.d.mts.map → Turn-Bi83du4I.d.mts.map} +1 -1
- package/dist/domain/Turn.d.mts +2 -2
- package/dist/domain/Turn.mjs +11 -7
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/language-model/LanguageModel.d.mts +1 -1
- package/dist/loop/Loop.d.mts +87 -2
- package/dist/loop/Loop.d.mts.map +1 -0
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/tool/HistoryCheck.d.mts +1 -1
- package/dist/tool/Outcome.d.mts +2 -2
- package/dist/tool/Outcome.mjs +4 -10
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +24 -22
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +45 -44
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +9 -9
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +11 -10
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/package.json +1 -1
- package/src/domain/Turn.ts +7 -17
- package/src/tool/Outcome.ts +3 -17
- package/src/tool/Resolvers.test.ts +39 -86
- package/src/tool/Resolvers.ts +74 -93
- package/src/tool/ToolEvent.ts +2 -2
- package/src/tool/Toolkit.ts +13 -39
- package/dist/Loop-CzSJo1h8.d.mts +0 -87
- package/dist/Loop-CzSJo1h8.d.mts.map +0 -1
- package/dist/Outcome-C2JYknCu.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ ESM-only. Requires `effect@4.x` as a peer.
|
|
|
35
35
|
|
|
36
36
|
Full docs: <https://effect-uai.betalyra.com>
|
|
37
37
|
|
|
38
|
-
Start with
|
|
38
|
+
Start with [One turn is a stream](https://effect-uai.betalyra.com/start/getting-started/)
|
|
39
39
|
and then [Basic usage](https://effect-uai.betalyra.com/recipes/basic-usage/).
|
|
40
40
|
|
|
41
41
|
## License
|
|
@@ -19,14 +19,6 @@ declare const isValue: (r: ToolResult) => r is Extract<ToolResult, {
|
|
|
19
19
|
declare const isFailure: (r: ToolResult) => r is Extract<ToolResult, {
|
|
20
20
|
_tag: "Failure";
|
|
21
21
|
}>;
|
|
22
|
-
type ToolDecision = {
|
|
23
|
-
readonly _tag: "Execute";
|
|
24
|
-
} | {
|
|
25
|
-
readonly _tag: "Reject";
|
|
26
|
-
readonly result: ToolResult;
|
|
27
|
-
};
|
|
28
|
-
declare const execute: ToolDecision;
|
|
29
|
-
declare const reject: (result: ToolResult) => ToolDecision;
|
|
30
22
|
declare const rejected: (call: FunctionCall, kind: string, reason?: string) => ToolResult;
|
|
31
23
|
/** Explicit user/policy rejection. */
|
|
32
24
|
declare const denied: (call: FunctionCall, reason?: string) => ToolResult;
|
|
@@ -36,5 +28,5 @@ declare const cancelled: (call: FunctionCall, reason?: string) => ToolResult;
|
|
|
36
28
|
declare const executionError: (call: FunctionCall, reason: string) => ToolResult;
|
|
37
29
|
declare const toFunctionCallOutput: (r: ToolResult) => FunctionCallOutput;
|
|
38
30
|
//#endregion
|
|
39
|
-
export {
|
|
40
|
-
//# sourceMappingURL=Outcome-
|
|
31
|
+
export { isFailure as a, toFunctionCallOutput as c, executionError as i, cancelled as n, isValue as o, denied as r, rejected as s, ToolResult as t };
|
|
32
|
+
//# sourceMappingURL=Outcome-GiaNvt7i.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Outcome-GiaNvt7i.d.mts","names":[],"sources":["../src/tool/Outcome.ts"],"mappings":";;;KAqBY,UAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,IAAA;EAAA,SACA,KAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,IAAA;EAAA,SACA,IAAA;EAAA,SACA,MAAA;AAAA;AAAA,cAGF,OAAA,GAAW,CAAA,EAAG,UAAA,KAAa,CAAA,IAAK,OAAA,CAAQ,UAAA;EAAc,IAAA;AAAA;AAAA,cAGtD,SAAA,GAAa,CAAA,EAAG,UAAA,KAAa,CAAA,IAAK,OAAA,CAAQ,UAAA;EAAc,IAAA;AAAA;AAAA,cAOxD,QAAA,GACX,IAAA,EAAM,YAAA,EACN,IAAA,UACA,MAAA,cACC,UAAA;;cASU,MAAA,GAAU,IAAA,EAAM,YAAA,EAAc,MAAA,cAAkB,UAAA;;cAIhD,SAAA,GAAa,IAAA,EAAM,YAAA,EAAc,MAAA,cAAkB,UAAA;;cAInD,cAAA,GAAkB,IAAA,EAAM,YAAA,EAAc,MAAA,aAAiB,UAAA;AAAA,cAOvD,oBAAA,GAAwB,CAAA,EAAG,UAAA,KAAa,kBAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as ToolResult } from "./Outcome-GiaNvt7i.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/tool/ToolEvent.d.ts
|
|
4
4
|
type ToolEvent = {
|
|
@@ -26,4 +26,4 @@ declare const isOutput: (e: ToolEvent) => e is Extract<ToolEvent, {
|
|
|
26
26
|
}>;
|
|
27
27
|
//#endregion
|
|
28
28
|
export { isOutput as i, isApprovalRequested as n, isIntermediate as r, ToolEvent as t };
|
|
29
|
-
//# sourceMappingURL=ToolEvent-
|
|
29
|
+
//# sourceMappingURL=ToolEvent-wTMgb2GO.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolEvent-
|
|
1
|
+
{"version":3,"file":"ToolEvent-wTMgb2GO.d.mts","names":[],"sources":["../src/tool/ToolEvent.ts"],"mappings":";;;KAYY,SAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,IAAA;EAAA,SACA,SAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,IAAA;EAAA,SACA,IAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SAAyB,MAAA,EAAQ,UAAA;AAAA;AAAA,cAEnC,mBAAA,GACX,CAAA,EAAG,SAAA,KACF,CAAA,IAAK,OAAA,CAAQ,SAAA;EAAa,IAAA;AAAA;AAAA,cAEhB,cAAA,GACX,CAAA,EAAG,SAAA,KACF,CAAA,IAAK,OAAA,CAAQ,SAAA;EAAa,IAAA;AAAA;AAAA,cAEhB,QAAA,GAAY,CAAA,EAAG,SAAA,KAAY,CAAA,IAAK,OAAA,CAAQ,SAAA;EAAa,IAAA;AAAA"}
|
|
@@ -6,7 +6,7 @@ import * as _$effect_Cause0 from "effect/Cause";
|
|
|
6
6
|
|
|
7
7
|
//#region src/domain/Turn.d.ts
|
|
8
8
|
declare namespace Turn_d_exports {
|
|
9
|
-
export {
|
|
9
|
+
export { InteractionEvent, RefusalRejected, Turn, TurnEvent, appendTurn, assistantMessages, functionCalls, isTurnComplete, reasonings, textDeltas, toStructured };
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* The result of a single LLM generation. A turn produces zero or more items
|
|
@@ -160,24 +160,13 @@ declare const functionCalls: (turn: Turn) => ReadonlyArray<FunctionCall>;
|
|
|
160
160
|
declare const reasonings: (turn: Turn) => ReadonlyArray<Reasoning>;
|
|
161
161
|
declare const assistantMessages: (turn: Turn) => ReadonlyArray<Message>;
|
|
162
162
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* around for stop-reason / usage / function-call inspection.
|
|
167
|
-
*
|
|
168
|
-
* Generic over the recipe's state shape - any record carrying a
|
|
169
|
-
* `history: ReadonlyArray<Item>` field works.
|
|
170
|
-
*/
|
|
171
|
-
type Cursor<S> = S & {
|
|
172
|
-
readonly turn: Turn;
|
|
173
|
-
};
|
|
174
|
-
/**
|
|
175
|
-
* Build a `Cursor<S>` from a state record and the just-completed turn.
|
|
176
|
-
* Extends `state.history` with `turn.items` and stamps the turn.
|
|
163
|
+
* Append a completed turn and optional follow-up items to a state record's
|
|
164
|
+
* history. Recipes use this at the point where structured tool results are
|
|
165
|
+
* converted to model-facing `FunctionCallOutput`s.
|
|
177
166
|
*/
|
|
178
|
-
declare const
|
|
167
|
+
declare const appendTurn: <S extends {
|
|
179
168
|
readonly history: ReadonlyArray<Item>;
|
|
180
|
-
}>(state: S, turn: Turn) =>
|
|
169
|
+
}>(state: S, turn: Turn, items?: ReadonlyArray<Item>) => S;
|
|
181
170
|
/**
|
|
182
171
|
* Project a `TurnEvent` stream onto its `text_delta` payloads. Other
|
|
183
172
|
* variants are dropped. Composes with `Lines.lines` +
|
|
@@ -207,5 +196,5 @@ declare class RefusalRejected extends RefusalRejected_base<{
|
|
|
207
196
|
*/
|
|
208
197
|
declare const toStructured: <A>(turn: Turn, format: StructuredFormat<A>) => Effect.Effect<A, RefusalRejected | JsonParseError | StructuredDecodeError>;
|
|
209
198
|
//#endregion
|
|
210
|
-
export {
|
|
211
|
-
//# sourceMappingURL=Turn-
|
|
199
|
+
export { Turn_d_exports as a, functionCalls as c, textDeltas as d, toStructured as f, TurnEvent as i, isTurnComplete as l, RefusalRejected as n, appendTurn as o, Turn as r, assistantMessages as s, InteractionEvent as t, reasonings as u };
|
|
200
|
+
//# sourceMappingURL=Turn-Bi83du4I.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Turn-
|
|
1
|
+
{"version":3,"file":"Turn-Bi83du4I.d.mts","names":[],"sources":["../src/domain/Turn.ts"],"mappings":";;;;;;;;;;;;;;;cAmBa,IAAA,EAAI,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAKL,IAAA,UAAc,IAAA,CAAK,IAAA;;;;;;;KAQnB,SAAA;EAAA,SACG,IAAA;EAAA,SAA6B,IAAA;AAAA;EAAA,SAE7B,IAAA;EAAA,SACA,IAAA;;;;;;;;WAQA,IAAA;AAAA;;;;;;;;;WASA,IAAA;EAAA,SAAgC,IAAA;AAAA;EAAA,SAChC,IAAA;EAAA,SAAkC,OAAA;EAAA,SAA0B,IAAA;AAAA;EAAA,SAC5D,IAAA;EAAA,SAAuC,OAAA;EAAA,SAA0B,KAAA;AAAA;;;;;;;;;;WASjE,IAAA;EAAA,SAA+B,KAAA,EAAO,KAAA;AAAA;EAAA,SACtC,IAAA;EAAA,SAAgC,IAAA,EAAM,IAAA;AAAA;;;;;;;KAQzC,gBAAA,GAAmB,SAAA,GAAY,kBAAA;AAAA,cAE9B,cAAA,GAAkB,CAAA,EAAG,SAAA,KAAY,CAAA,IAAK,OAAA,CAAQ,SAAA;EAAa,IAAA;AAAA;AAAA,cAG3D,aAAA,GAAiB,IAAA,EAAM,IAAA,KAAO,aAAA,CAAc,YAAA;AAAA,cAG5C,UAAA,GAAc,IAAA,EAAM,IAAA,KAAO,aAAA,CAAc,SAAA;AAAA,cAGzC,iBAAA,GAAqB,IAAA,EAAM,IAAA,KAAO,aAAA,CAAc,OAAA;;;;;;cAQhD,UAAA;EAAA,SAAmC,OAAA,EAAS,aAAA,CAAc,IAAA;AAAA,GACrE,KAAA,EAAO,CAAA,EACP,IAAA,EAAM,IAAA,EACN,KAAA,GAAO,aAAA,CAAc,IAAA,MACpB,CAAA;;;;;;cAcU,UAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA,MACjC,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA;AAAA,cAKzB,oBAAA;;;;;;;;cAWU,eAAA,SAAwB,oBAAA;EAAA,SAC1B,IAAA,EAAM,IAAA;AAAA;;;;;;;;;;;cAyBJ,YAAA,MACX,IAAA,EAAM,IAAA,EACN,MAAA,EAAQ,gBAAA,CAAkC,CAAA,MACzC,MAAA,CAAO,MAAA,CACR,CAAA,EACA,eAAA,GAAkB,cAAA,GAAkC,qBAAA"}
|
package/dist/domain/Turn.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { c as functionCalls, d as textDeltas, f as toStructured, i as TurnEvent, l as isTurnComplete, n as RefusalRejected, o as appendTurn, r as Turn, s as assistantMessages, t as InteractionEvent, u as reasonings } from "../Turn-Bi83du4I.mjs";
|
|
2
|
+
export { InteractionEvent, RefusalRejected, Turn, TurnEvent, appendTurn, assistantMessages, functionCalls, isTurnComplete, reasonings, textDeltas, toStructured };
|
package/dist/domain/Turn.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import { Data, Effect, Result, Schema, Stream, pipe } from "effect";
|
|
|
6
6
|
var Turn_exports = /* @__PURE__ */ __exportAll({
|
|
7
7
|
RefusalRejected: () => RefusalRejected,
|
|
8
8
|
Turn: () => Turn,
|
|
9
|
+
appendTurn: () => appendTurn,
|
|
9
10
|
assistantMessages: () => assistantMessages,
|
|
10
|
-
cursor: () => cursor,
|
|
11
11
|
functionCalls: () => functionCalls,
|
|
12
12
|
isTurnComplete: () => isTurnComplete,
|
|
13
13
|
reasonings: () => reasonings,
|
|
@@ -29,13 +29,17 @@ const functionCalls = (turn) => turn.items.filter((i) => i.type === "function_ca
|
|
|
29
29
|
const reasonings = (turn) => turn.items.filter((i) => i.type === "reasoning");
|
|
30
30
|
const assistantMessages = (turn) => turn.items.filter((i) => i.type === "message" && i.role === "assistant");
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Append a completed turn and optional follow-up items to a state record's
|
|
33
|
+
* history. Recipes use this at the point where structured tool results are
|
|
34
|
+
* converted to model-facing `FunctionCallOutput`s.
|
|
34
35
|
*/
|
|
35
|
-
const
|
|
36
|
+
const appendTurn = (state, turn, items = []) => ({
|
|
36
37
|
...state,
|
|
37
|
-
history: [
|
|
38
|
-
|
|
38
|
+
history: [
|
|
39
|
+
...state.history,
|
|
40
|
+
...turn.items,
|
|
41
|
+
...items
|
|
42
|
+
]
|
|
39
43
|
});
|
|
40
44
|
/**
|
|
41
45
|
* Project a `TurnEvent` stream onto its `text_delta` payloads. Other
|
|
@@ -77,6 +81,6 @@ const lastAssistantContent = (turn) => {
|
|
|
77
81
|
*/
|
|
78
82
|
const toStructured = (turn, format) => pipe(lastAssistantContent(turn), ({ text, refused }) => refused ? Effect.fail(new RefusalRejected({ turn })) : parseJson(format)(text));
|
|
79
83
|
//#endregion
|
|
80
|
-
export { RefusalRejected, Turn,
|
|
84
|
+
export { RefusalRejected, Turn, appendTurn, assistantMessages, functionCalls, isTurnComplete, reasonings, Turn_exports as t, textDeltas, toStructured };
|
|
81
85
|
|
|
82
86
|
//# sourceMappingURL=Turn.mjs.map
|
package/dist/domain/Turn.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Turn.mjs","names":["StructuredFormat.parseJson"],"sources":["../../src/domain/Turn.ts"],"sourcesContent":["import { Data, Effect, Result, Schema, Stream, pipe } from \"effect\"\nimport * as StructuredFormat from \"../structured-format/StructuredFormat.js\"\nimport {\n FunctionCall,\n FunctionCallOutput,\n Item,\n isOutputText,\n isRefusal,\n Message,\n Reasoning,\n StopReason,\n Usage,\n} from \"./Items.js\"\n\n/**\n * The result of a single LLM generation. A turn produces zero or more items\n * (typically one assistant message and zero or more function_call items)\n * and reports usage + a stop reason.\n */\nexport const Turn = Schema.Struct({\n items: Schema.Array(Item),\n usage: Usage,\n stop_reason: StopReason,\n})\nexport type Turn = typeof Turn.Type\n\n/**\n * Canonical events emitted while a single turn is being generated. Most\n * variants are streaming deltas (text, reasoning, tool-call args); the\n * terminal `turn_complete` carries the assembled `Turn`. Lifecycle members\n * aren't deltas, hence the union name.\n */\nexport type TurnEvent =\n | { readonly type: \"text_delta\"; readonly text: string }\n | {\n readonly type: \"reasoning_delta\"\n readonly text: string\n /**\n * `trace` is the model's raw chain-of-thought; `summary` is a\n * model-written summary intended for display. OpenAI Responses emits\n * both as separate wire events; Anthropic and Gemini only emit\n * `trace`. Consumers who just want any reasoning text match once;\n * those who want only summaries filter `kind === \"summary\"`.\n */\n readonly kind: \"trace\" | \"summary\"\n }\n /**\n * The model declined to answer. `text` is the (streamed) explanation.\n * Distinct from the failure channel: a refusal is normal model output and\n * the stream still completes with `turn_complete`. OpenAI Responses emits\n * this; Anthropic surfaces refusals via `stop_reason`, and Gemini collapses\n * them into `finishReason: SAFETY` - both go without a `refusal_delta`.\n */\n | { readonly type: \"refusal_delta\"; readonly text: string }\n | { readonly type: \"tool_call_start\"; readonly call_id: string; readonly name: string }\n | { readonly type: \"tool_call_args_delta\"; readonly call_id: string; readonly delta: string }\n /**\n * Mid-stream cumulative usage. Carries the full `Usage` (including cache\n * token fields when the provider surfaces them) so consumers can drive\n * live budget / cost tracking without waiting for `turn_complete`.\n * Anthropic emits this on `message_start` and `message_delta`; other\n * providers may not emit any `usage_update` and only deliver usage via\n * `turn_complete.turn.usage`.\n */\n | { readonly type: \"usage_update\"; readonly usage: Usage }\n | { readonly type: \"turn_complete\"; readonly turn: Turn }\n\n/**\n * What flows out of an agent loop body to its consumer per turn: every\n * `TurnEvent` the provider emits (including the terminal `turn_complete`\n * carrying the assembled `Turn`), plus the output of any tool the loop ran.\n * Both variants carry a `type` discriminator.\n */\nexport type InteractionEvent = TurnEvent | FunctionCallOutput\n\nexport const isTurnComplete = (d: TurnEvent): d is Extract<TurnEvent, { type: \"turn_complete\" }> =>\n d.type === \"turn_complete\"\n\nexport const functionCalls = (turn: Turn): ReadonlyArray<FunctionCall> =>\n turn.items.filter((i): i is FunctionCall => i.type === \"function_call\")\n\nexport const reasonings = (turn: Turn): ReadonlyArray<Reasoning> =>\n turn.items.filter((i): i is Reasoning => i.type === \"reasoning\")\n\nexport const assistantMessages = (turn: Turn): ReadonlyArray<Message> =>\n turn.items.filter((i): i is Message => i.type === \"message\" && i.role === \"assistant\")\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"Turn.mjs","names":["StructuredFormat.parseJson"],"sources":["../../src/domain/Turn.ts"],"sourcesContent":["import { Data, Effect, Result, Schema, Stream, pipe } from \"effect\"\nimport * as StructuredFormat from \"../structured-format/StructuredFormat.js\"\nimport {\n FunctionCall,\n FunctionCallOutput,\n Item,\n isOutputText,\n isRefusal,\n Message,\n Reasoning,\n StopReason,\n Usage,\n} from \"./Items.js\"\n\n/**\n * The result of a single LLM generation. A turn produces zero or more items\n * (typically one assistant message and zero or more function_call items)\n * and reports usage + a stop reason.\n */\nexport const Turn = Schema.Struct({\n items: Schema.Array(Item),\n usage: Usage,\n stop_reason: StopReason,\n})\nexport type Turn = typeof Turn.Type\n\n/**\n * Canonical events emitted while a single turn is being generated. Most\n * variants are streaming deltas (text, reasoning, tool-call args); the\n * terminal `turn_complete` carries the assembled `Turn`. Lifecycle members\n * aren't deltas, hence the union name.\n */\nexport type TurnEvent =\n | { readonly type: \"text_delta\"; readonly text: string }\n | {\n readonly type: \"reasoning_delta\"\n readonly text: string\n /**\n * `trace` is the model's raw chain-of-thought; `summary` is a\n * model-written summary intended for display. OpenAI Responses emits\n * both as separate wire events; Anthropic and Gemini only emit\n * `trace`. Consumers who just want any reasoning text match once;\n * those who want only summaries filter `kind === \"summary\"`.\n */\n readonly kind: \"trace\" | \"summary\"\n }\n /**\n * The model declined to answer. `text` is the (streamed) explanation.\n * Distinct from the failure channel: a refusal is normal model output and\n * the stream still completes with `turn_complete`. OpenAI Responses emits\n * this; Anthropic surfaces refusals via `stop_reason`, and Gemini collapses\n * them into `finishReason: SAFETY` - both go without a `refusal_delta`.\n */\n | { readonly type: \"refusal_delta\"; readonly text: string }\n | { readonly type: \"tool_call_start\"; readonly call_id: string; readonly name: string }\n | { readonly type: \"tool_call_args_delta\"; readonly call_id: string; readonly delta: string }\n /**\n * Mid-stream cumulative usage. Carries the full `Usage` (including cache\n * token fields when the provider surfaces them) so consumers can drive\n * live budget / cost tracking without waiting for `turn_complete`.\n * Anthropic emits this on `message_start` and `message_delta`; other\n * providers may not emit any `usage_update` and only deliver usage via\n * `turn_complete.turn.usage`.\n */\n | { readonly type: \"usage_update\"; readonly usage: Usage }\n | { readonly type: \"turn_complete\"; readonly turn: Turn }\n\n/**\n * What flows out of an agent loop body to its consumer per turn: every\n * `TurnEvent` the provider emits (including the terminal `turn_complete`\n * carrying the assembled `Turn`), plus the output of any tool the loop ran.\n * Both variants carry a `type` discriminator.\n */\nexport type InteractionEvent = TurnEvent | FunctionCallOutput\n\nexport const isTurnComplete = (d: TurnEvent): d is Extract<TurnEvent, { type: \"turn_complete\" }> =>\n d.type === \"turn_complete\"\n\nexport const functionCalls = (turn: Turn): ReadonlyArray<FunctionCall> =>\n turn.items.filter((i): i is FunctionCall => i.type === \"function_call\")\n\nexport const reasonings = (turn: Turn): ReadonlyArray<Reasoning> =>\n turn.items.filter((i): i is Reasoning => i.type === \"reasoning\")\n\nexport const assistantMessages = (turn: Turn): ReadonlyArray<Message> =>\n turn.items.filter((i): i is Message => i.type === \"message\" && i.role === \"assistant\")\n\n/**\n * Append a completed turn and optional follow-up items to a state record's\n * history. Recipes use this at the point where structured tool results are\n * converted to model-facing `FunctionCallOutput`s.\n */\nexport const appendTurn = <S extends { readonly history: ReadonlyArray<Item> }>(\n state: S,\n turn: Turn,\n items: ReadonlyArray<Item> = [],\n): S => ({\n ...state,\n history: [...state.history, ...turn.items, ...items],\n})\n\n// ---------------------------------------------------------------------------\n// Stream operators\n// ---------------------------------------------------------------------------\n\n/**\n * Project a `TurnEvent` stream onto its `text_delta` payloads. Other\n * variants are dropped. Composes with `Lines.lines` +\n * `decodeJsonLines` for prompted-JSONL streaming.\n */\nexport const textDeltas = <E, R>(\n self: Stream.Stream<TurnEvent, E, R>,\n): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.filterMap((ev) =>\n ev.type === \"text_delta\" ? Result.succeed(ev.text) : Result.failVoid,\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Structured-output integration\n// ---------------------------------------------------------------------------\n\n/**\n * The assistant message on the just-completed turn was a refusal block,\n * not an `output_text` payload. Returned by `toStructured` to short-circuit\n * decoding before `JSON.parse` / schema validation runs.\n */\nexport class RefusalRejected extends Data.TaggedError(\"RefusalRejected\")<{\n readonly turn: Turn\n}> {}\n\nconst lastAssistantContent = (turn: Turn): { readonly text: string; readonly refused: boolean } => {\n const assistants = assistantMessages(turn)\n const last = assistants[assistants.length - 1]\n if (last === undefined) return { text: \"\", refused: false }\n if (last.content.some(isRefusal)) return { text: \"\", refused: true }\n const text = last.content\n .filter(isOutputText)\n .map((b) => b.text)\n .join(\"\")\n return { text, refused: false }\n}\n\n/**\n * Validate a completed `Turn` against a `StructuredFormat`. Concatenates\n * `output_text` blocks on the last assistant message, then runs\n * `JSON.parse` + the format's schema validation.\n *\n * Three failure modes:\n * - `RefusalRejected` — the assistant emitted a refusal block.\n * - `JsonParseError` — the assembled text wasn't valid JSON.\n * - `StructuredDecodeError` — the JSON didn't match the schema.\n */\nexport const toStructured = <A>(\n turn: Turn,\n format: StructuredFormat.StructuredFormat<A>,\n): Effect.Effect<\n A,\n RefusalRejected | StructuredFormat.JsonParseError | StructuredFormat.StructuredDecodeError\n> =>\n pipe(lastAssistantContent(turn), ({ text, refused }) =>\n refused ? Effect.fail(new RefusalRejected({ turn })) : StructuredFormat.parseJson(format)(text),\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmBA,MAAa,OAAO,OAAO,OAAO;CAChC,OAAO,OAAO,MAAM,KAAK;CACzB,OAAO;CACP,aAAa;CACd,CAAC;AAoDF,MAAa,kBAAkB,MAC7B,EAAE,SAAS;AAEb,MAAa,iBAAiB,SAC5B,KAAK,MAAM,QAAQ,MAAyB,EAAE,SAAS,gBAAgB;AAEzE,MAAa,cAAc,SACzB,KAAK,MAAM,QAAQ,MAAsB,EAAE,SAAS,YAAY;AAElE,MAAa,qBAAqB,SAChC,KAAK,MAAM,QAAQ,MAAoB,EAAE,SAAS,aAAa,EAAE,SAAS,YAAY;;;;;;AAOxF,MAAa,cACX,OACA,MACA,QAA6B,EAAE,MACxB;CACP,GAAG;CACH,SAAS;EAAC,GAAG,MAAM;EAAS,GAAG,KAAK;EAAO,GAAG;EAAM;CACrD;;;;;;AAWD,MAAa,cACX,SAEA,KAAK,KACH,OAAO,WAAW,OAChB,GAAG,SAAS,eAAe,OAAO,QAAQ,GAAG,KAAK,GAAG,OAAO,SAC7D,CACF;;;;;;AAWH,IAAa,kBAAb,cAAqC,KAAK,YAAY,kBAAkB,CAErE;AAEH,MAAM,wBAAwB,SAAqE;CACjG,MAAM,aAAa,kBAAkB,KAAK;CAC1C,MAAM,OAAO,WAAW,WAAW,SAAS;AAC5C,KAAI,SAAS,KAAA,EAAW,QAAO;EAAE,MAAM;EAAI,SAAS;EAAO;AAC3D,KAAI,KAAK,QAAQ,KAAK,UAAU,CAAE,QAAO;EAAE,MAAM;EAAI,SAAS;EAAM;AAKpE,QAAO;EAAE,MAJI,KAAK,QACf,OAAO,aAAa,CACpB,KAAK,MAAM,EAAE,KAAK,CAClB,KAAK,GACK;EAAE,SAAS;EAAO;;;;;;;;;;;;AAajC,MAAa,gBACX,MACA,WAKA,KAAK,qBAAqB,KAAK,GAAG,EAAE,MAAM,cACxC,UAAU,OAAO,KAAK,IAAI,gBAAgB,EAAE,MAAM,CAAC,CAAC,GAAGA,UAA2B,OAAO,CAAC,KAAK,CAChG"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { n as AiError_d_exports } from "./AiError-CqmYjXyx.mjs";
|
|
2
2
|
import { h as Items_d_exports } from "./Items-D1C2686t.mjs";
|
|
3
3
|
import { a as StructuredFormat_d_exports } from "./StructuredFormat-B5ueioNr.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { a as Turn_d_exports } from "./Turn-Bi83du4I.mjs";
|
|
5
5
|
import { l as Tool_d_exports } from "./Tool-5wxOCuOh.mjs";
|
|
6
6
|
import { t as LanguageModel_d_exports } from "./language-model/LanguageModel.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { t as Loop_d_exports } from "./loop/Loop.mjs";
|
|
8
8
|
import { t as Match_d_exports } from "./match/Match.mjs";
|
|
9
9
|
import { t as Toolkit_d_exports } from "./tool/Toolkit.mjs";
|
|
10
10
|
import { t as JSONL_d_exports } from "./streaming/JSONL.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as AiError } from "../AiError-CqmYjXyx.mjs";
|
|
2
2
|
import { m as Item } from "../Items-D1C2686t.mjs";
|
|
3
3
|
import { i as StructuredFormat } from "../StructuredFormat-B5ueioNr.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { i as TurnEvent, r as Turn } from "../Turn-Bi83du4I.mjs";
|
|
5
5
|
import { o as ToolDescriptor } from "../Tool-5wxOCuOh.mjs";
|
|
6
6
|
import { Context, Effect, Stream } from "effect";
|
|
7
7
|
|
package/dist/loop/Loop.d.mts
CHANGED
|
@@ -1,2 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { l as IncompleteTurn } from "../AiError-CqmYjXyx.mjs";
|
|
2
|
+
import { i as TurnEvent, r as Turn } from "../Turn-Bi83du4I.mjs";
|
|
3
|
+
import { Data, Effect, Stream } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/loop/Loop.d.ts
|
|
6
|
+
declare namespace Loop_d_exports {
|
|
7
|
+
export { Event, loop, next, nextAfter, nextAfterFold, stop, stopAfter, stopEvent, streamUntilComplete, value };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The tagged union a body emits per pull. `Value` carries a payload that
|
|
11
|
+
* flows downstream. `Next` ends the current iteration and continues with a
|
|
12
|
+
* new state. `Stop` ends the loop entirely.
|
|
13
|
+
*/
|
|
14
|
+
type Event<A, S> = Data.TaggedEnum<{
|
|
15
|
+
Value: {
|
|
16
|
+
readonly value: A;
|
|
17
|
+
};
|
|
18
|
+
Next: {
|
|
19
|
+
readonly state: S;
|
|
20
|
+
};
|
|
21
|
+
Stop: {};
|
|
22
|
+
}>;
|
|
23
|
+
/** Wrap a value so it flows through the loop to downstream consumers. */
|
|
24
|
+
declare const value: <A>(a: A) => Event<A, never>;
|
|
25
|
+
/** End the current iteration and continue with a new state. */
|
|
26
|
+
declare const next: <S>(state: S) => Event<never, S>;
|
|
27
|
+
/** The terminal `Stop` event. Use `stop` (the Stream) to end a loop body. */
|
|
28
|
+
declare const stopEvent: Event<never, never>;
|
|
29
|
+
/**
|
|
30
|
+
* A single-element stream that ends the loop. Return this from a body when
|
|
31
|
+
* there's nothing else to emit; equivalent to `stopAfter(Stream.empty)` but
|
|
32
|
+
* named for the common case.
|
|
33
|
+
*/
|
|
34
|
+
declare const stop: Stream.Stream<Event<never, never>>;
|
|
35
|
+
/**
|
|
36
|
+
* Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the
|
|
37
|
+
* iteration with `next(state)`. Common shape for "stream this turn's
|
|
38
|
+
* deltas, then continue with updated history."
|
|
39
|
+
*/
|
|
40
|
+
declare const nextAfter: <S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S) => Stream.Stream<Event<A, S>, E, R>;
|
|
41
|
+
/**
|
|
42
|
+
* Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the
|
|
43
|
+
* loop. Common shape for "stream this turn's deltas, then we're done."
|
|
44
|
+
*/
|
|
45
|
+
declare const stopAfter: <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<Event<A, never>, E, R>;
|
|
46
|
+
/**
|
|
47
|
+
* General `nextAfter` variant: drain `stream` to the consumer, fold elements
|
|
48
|
+
* into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.
|
|
49
|
+
*
|
|
50
|
+
* Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,
|
|
51
|
+
* `build: (s) => s`). Used by `Toolkit.nextStateFrom` to collect tool
|
|
52
|
+
* results and build next state without exposing a Ref to recipes.
|
|
53
|
+
*/
|
|
54
|
+
declare const nextAfterFold: <A, B, S, E, R>(stream: Stream.Stream<A, E, R>, initial: B, reduce: (acc: B, a: A) => B, build: (b: B) => S) => Stream.Stream<Event<A, S>, E, R>;
|
|
55
|
+
/**
|
|
56
|
+
* Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Event<TurnEvent | A, S>>`.
|
|
57
|
+
* Each delta passes through as `value(delta)` (including the terminal
|
|
58
|
+
* `turn_complete`, so the consumer sees turn boundaries naturally). Once
|
|
59
|
+
* the terminal arrives, `then(turn)` runs and its returned stream of loop
|
|
60
|
+
* events (typically tool outputs followed by `next(state)` or `stop`) is
|
|
61
|
+
* concatenated.
|
|
62
|
+
*
|
|
63
|
+
* Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on
|
|
64
|
+
* the raw delta stream cover anything an `emit`-style callback would do.
|
|
65
|
+
*
|
|
66
|
+
* If the upstream ends without a `turn_complete`, the resulting stream
|
|
67
|
+
* fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if
|
|
68
|
+
* you want to recover.
|
|
69
|
+
*/
|
|
70
|
+
declare const streamUntilComplete: <S, A, E2 = never, R2 = never>(then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>) => <E, R>(deltas: Stream.Stream<TurnEvent, E, R>) => Stream.Stream<Event<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>;
|
|
71
|
+
type LoopBody<S, A, E, R> = (state: S) => Stream.Stream<Event<A, S>, E, R> | Effect.Effect<Stream.Stream<Event<A, S>, E, R>, E, R>;
|
|
72
|
+
/**
|
|
73
|
+
* Drive a state-threaded loop body. Each iteration runs `body(state)` to get
|
|
74
|
+
* a `Stream<Event<A, S>>`; values flow downstream, `next(s)` continues with
|
|
75
|
+
* a new state, `stop` ends the loop. See the file header for the full
|
|
76
|
+
* pull-based execution model.
|
|
77
|
+
*
|
|
78
|
+
* Dual: data-first `loop(initial, body)` and data-last `loop(body)(initial)`
|
|
79
|
+
* (or `pipe(initial, loop(body))`) both work.
|
|
80
|
+
*/
|
|
81
|
+
declare const loop: {
|
|
82
|
+
<S, A, E, R>(body: LoopBody<S, A, E, R>): (initial: S) => Stream.Stream<A, E, R>;
|
|
83
|
+
<S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R>;
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { Event, loop, next, nextAfter, nextAfterFold, stop, stopAfter, stopEvent, streamUntilComplete, Loop_d_exports as t, value };
|
|
87
|
+
//# sourceMappingURL=Loop.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Loop.d.mts","names":[],"sources":["../../src/loop/Loop.ts"],"mappings":";;;;;;;;;;;;;KAgCY,KAAA,SAAc,IAAA,CAAK,UAAA;EAC7B,KAAA;IAAA,SAAkB,KAAA,EAAO,CAAA;EAAA;EACzB,IAAA;IAAA,SAAiB,KAAA,EAAO,CAAA;EAAA;EACxB,IAAA;AAAA;;cAUW,KAAA,MAAY,CAAA,EAAG,CAAA,KAAI,KAAA,CAAM,CAAA;;cAGzB,IAAA,MAAW,KAAA,EAAO,CAAA,KAAI,KAAA,QAAa,CAAA;;cAGnC,SAAA,EAAW,KAAA;;;;;;cAOX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,KAAA;;;;;;cAOpB,SAAA,eACX,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,GAC5B,KAAA,EAAO,CAAA,KACN,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA;AApBjC;;;;AAAA,cA2Ba,SAAA,YACX,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAC3B,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,CAAA,UAAW,CAAA,EAAG,CAAA;;;;;;;;;cAWxB,aAAA,kBACX,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,GAC5B,OAAA,EAAS,CAAA,EACT,MAAA,GAAS,GAAA,EAAK,CAAA,EAAG,CAAA,EAAG,CAAA,KAAM,CAAA,EAC1B,KAAA,GAAQ,CAAA,EAAG,CAAA,KAAM,CAAA,KAChB,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA;;AA1CjC;;;;;AAOA;;;;;;;;;cAqEa,mBAAA,iCAET,IAAA,GAAO,IAAA,EAAM,IAAA,KAAS,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,aAG5E,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA,MACnC,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,SAAA,GAAY,CAAA,EAAG,CAAA,GAAI,CAAA,GAAI,EAAA,GAAK,cAAA,EAAgB,CAAA,GAAI,EAAA;AAAA,KAkEpE,QAAA,gBACH,KAAA,EAAO,CAAA,KACJ,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,IAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA;;;;;;;;;;cAW9E,IAAA;EAAA,aACE,IAAA,EAAM,QAAA,CAAS,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,KAAM,OAAA,EAAS,CAAA,KAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA;EAAA,aACjE,OAAA,EAAS,CAAA,EAAG,IAAA,EAAM,QAAA,CAAS,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,IAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA;AAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { m as Item } from "../Items-D1C2686t.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as Turn } from "../Turn-Bi83du4I.mjs";
|
|
3
3
|
import { LanguageModel, LanguageModelService } from "../language-model/LanguageModel.mjs";
|
|
4
4
|
import { Duration, Effect, Layer } from "effect";
|
|
5
5
|
|
package/dist/tool/Outcome.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export {
|
|
1
|
+
import { a as isFailure, c as toFunctionCallOutput, i as executionError, n as cancelled, o as isValue, r as denied, s as rejected, t as ToolResult } from "../Outcome-GiaNvt7i.mjs";
|
|
2
|
+
export { ToolResult, cancelled, denied, executionError, isFailure, isValue, rejected, toFunctionCallOutput };
|
package/dist/tool/Outcome.mjs
CHANGED
|
@@ -2,11 +2,10 @@ import { functionCallOutput } from "../domain/Items.mjs";
|
|
|
2
2
|
import { Match } from "effect";
|
|
3
3
|
//#region src/tool/Outcome.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* (`ToolResult`) for the resolver-based executor.
|
|
5
|
+
* Post-execution and synthetic tool results.
|
|
7
6
|
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
7
|
+
* - Executed tools emit ToolResult.Value.
|
|
8
|
+
* - Approval/cancellation policy emits synthetic ToolResult.Failure.
|
|
10
9
|
*
|
|
11
10
|
* Wire conversion stays at the recipe boundary via `toFunctionCallOutput`
|
|
12
11
|
* so recipes can inspect, redact, or audit values before serialization.
|
|
@@ -17,11 +16,6 @@ import { Match } from "effect";
|
|
|
17
16
|
*/
|
|
18
17
|
const isValue = (r) => r._tag === "Value";
|
|
19
18
|
const isFailure = (r) => r._tag === "Failure";
|
|
20
|
-
const execute = { _tag: "Execute" };
|
|
21
|
-
const reject = (result) => ({
|
|
22
|
-
_tag: "Reject",
|
|
23
|
-
result
|
|
24
|
-
});
|
|
25
19
|
const rejected = (call, kind, reason) => ({
|
|
26
20
|
_tag: "Failure",
|
|
27
21
|
call_id: call.call_id,
|
|
@@ -40,6 +34,6 @@ const toFunctionCallOutput = (r) => Match.value(r).pipe(Match.tag("Value", (v) =
|
|
|
40
34
|
reason: f.reason
|
|
41
35
|
} : { kind: f.kind }))), Match.exhaustive);
|
|
42
36
|
//#endregion
|
|
43
|
-
export { cancelled, denied,
|
|
37
|
+
export { cancelled, denied, executionError, isFailure, isValue, rejected, toFunctionCallOutput };
|
|
44
38
|
|
|
45
39
|
//# sourceMappingURL=Outcome.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Outcome.mjs","names":[],"sources":["../../src/tool/Outcome.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"Outcome.mjs","names":[],"sources":["../../src/tool/Outcome.ts"],"sourcesContent":["/**\n * Post-execution and synthetic tool results.\n *\n * - Executed tools emit ToolResult.Value.\n * - Approval/cancellation policy emits synthetic ToolResult.Failure.\n *\n * Wire conversion stays at the recipe boundary via `toFunctionCallOutput`\n * so recipes can inspect, redact, or audit values before serialization.\n *\n * `output` and `reason` are `string`, not `unknown`: the wire wants strings,\n * and `unknown` would invite non-serializable values (Date, Map, BigInt,\n * fn). Recipes that want structured detail JSON.stringify themselves.\n */\nimport { Match } from \"effect\"\nimport type { FunctionCall, FunctionCallOutput } from \"../domain/Items.js\"\nimport { functionCallOutput } from \"../domain/Items.js\"\n\n// ---------------------------------------------------------------------------\n// ToolResult\n// ---------------------------------------------------------------------------\n\nexport type ToolResult =\n | {\n readonly _tag: \"Value\"\n readonly call_id: string\n readonly tool: string\n readonly value: unknown\n }\n | {\n readonly _tag: \"Failure\"\n readonly call_id: string\n readonly tool: string\n readonly kind: string\n readonly reason?: string\n }\n\nexport const isValue = (r: ToolResult): r is Extract<ToolResult, { _tag: \"Value\" }> =>\n r._tag === \"Value\"\n\nexport const isFailure = (r: ToolResult): r is Extract<ToolResult, { _tag: \"Failure\" }> =>\n r._tag === \"Failure\"\n\n// Synthesizers. `denied` and `cancelled` are operationally distinct;\n// anything else is just a recipe-chosen `kind` via `rejected`.\n// ---------------------------------------------------------------------------\n\nexport const rejected = (\n call: FunctionCall,\n kind: string,\n reason?: string,\n): ToolResult => ({\n _tag: \"Failure\",\n call_id: call.call_id,\n tool: call.name,\n kind,\n ...(reason !== undefined ? { reason } : {}),\n})\n\n/** Explicit user/policy rejection. */\nexport const denied = (call: FunctionCall, reason?: string): ToolResult =>\n rejected(call, \"denied\", reason)\n\n/** Implicit non-answer (follow-up, inactivity, abort). */\nexport const cancelled = (call: FunctionCall, reason?: string): ToolResult =>\n rejected(call, \"cancelled\", reason)\n\n/** Tool's own execution failed (parse error, schema, runtime crash). */\nexport const executionError = (call: FunctionCall, reason: string): ToolResult =>\n rejected(call, \"execution_error\", reason)\n\n// ---------------------------------------------------------------------------\n// Wire conversion - the one place structured → string happens.\n// ---------------------------------------------------------------------------\n\nexport const toFunctionCallOutput = (r: ToolResult): FunctionCallOutput =>\n Match.value(r).pipe(\n Match.tag(\"Value\", (v) => functionCallOutput(v.call_id, JSON.stringify(v.value))),\n Match.tag(\"Failure\", (f) =>\n functionCallOutput(\n f.call_id,\n JSON.stringify(\n f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },\n ),\n ),\n ),\n Match.exhaustive,\n )\n"],"mappings":";;;;;;;;;;;;;;;;AAoCA,MAAa,WAAW,MACtB,EAAE,SAAS;AAEb,MAAa,aAAa,MACxB,EAAE,SAAS;AAMb,MAAa,YACX,MACA,MACA,YACgB;CAChB,MAAM;CACN,SAAS,KAAK;CACd,MAAM,KAAK;CACX;CACA,GAAI,WAAW,KAAA,IAAY,EAAE,QAAQ,GAAG,EAAE;CAC3C;;AAGD,MAAa,UAAU,MAAoB,WACzC,SAAS,MAAM,UAAU,OAAO;;AAGlC,MAAa,aAAa,MAAoB,WAC5C,SAAS,MAAM,aAAa,OAAO;;AAGrC,MAAa,kBAAkB,MAAoB,WACjD,SAAS,MAAM,mBAAmB,OAAO;AAM3C,MAAa,wBAAwB,MACnC,MAAM,MAAM,EAAE,CAAC,KACb,MAAM,IAAI,UAAU,MAAM,mBAAmB,EAAE,SAAS,KAAK,UAAU,EAAE,MAAM,CAAC,CAAC,EACjF,MAAM,IAAI,YAAY,MACpB,mBACE,EAAE,SACF,KAAK,UACH,EAAE,WAAW,KAAA,IAAY;CAAE,MAAM,EAAE;CAAM,QAAQ,EAAE;CAAQ,GAAG,EAAE,MAAM,EAAE,MAAM,CAC/E,CACF,CACF,EACD,MAAM,WACP"}
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import { o as FunctionCall } from "../Items-D1C2686t.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { t as ToolEvent } from "../ToolEvent-
|
|
4
|
-
import { Resolver } from "./Toolkit.mjs";
|
|
2
|
+
import { t as ToolResult } from "../Outcome-GiaNvt7i.mjs";
|
|
3
|
+
import { t as ToolEvent } from "../ToolEvent-wTMgb2GO.mjs";
|
|
5
4
|
import { Effect, Queue, Scope, Stream } from "effect";
|
|
6
5
|
|
|
7
6
|
//#region src/tool/Resolvers.d.ts
|
|
7
|
+
interface ToolCallPlan {
|
|
8
|
+
readonly approved: ReadonlyArray<FunctionCall>;
|
|
9
|
+
readonly rejected: ReadonlyArray<ToolResult>;
|
|
10
|
+
}
|
|
11
|
+
type ToolCallDecision = {
|
|
12
|
+
readonly _tag: "Approved";
|
|
13
|
+
readonly call: FunctionCall;
|
|
14
|
+
} | {
|
|
15
|
+
readonly _tag: "Rejected";
|
|
16
|
+
readonly result: ToolResult;
|
|
17
|
+
};
|
|
18
|
+
declare const approve: (call: FunctionCall) => ToolCallDecision;
|
|
19
|
+
declare const reject: (result: ToolResult) => ToolCallDecision;
|
|
20
|
+
declare const splitToolCallDecisions: (decisions: ReadonlyArray<ToolCallDecision>) => ToolCallPlan;
|
|
21
|
+
declare const approvalRequested: (call: FunctionCall) => ToolEvent;
|
|
8
22
|
interface Verdict {
|
|
9
23
|
readonly call_id: string;
|
|
10
24
|
readonly decision: "approve" | "deny";
|
|
11
25
|
readonly reason?: string;
|
|
12
26
|
}
|
|
13
27
|
/**
|
|
14
|
-
* Queue-backed
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* merges the announce stream into its consumer view.
|
|
28
|
+
* Queue-backed approval planner. Safe calls are returned immediately in
|
|
29
|
+
* `approved`; gated calls emit `ApprovalRequested` events and later produce
|
|
30
|
+
* one `ToolCallDecision` when their matching verdict arrives.
|
|
18
31
|
*/
|
|
19
32
|
declare const fromVerdictQueue: (predicate: (call: FunctionCall) => boolean, verdicts: Queue.Dequeue<Verdict>) => (calls: ReadonlyArray<FunctionCall>) => Effect.Effect<{
|
|
20
|
-
readonly
|
|
33
|
+
readonly approved: ReadonlyArray<FunctionCall>;
|
|
34
|
+
readonly decisions: Stream.Stream<ToolCallDecision>;
|
|
21
35
|
readonly announce: Stream.Stream<ToolEvent>;
|
|
22
36
|
}, never, Scope.Scope>;
|
|
23
37
|
type ApprovalMapEntry = {
|
|
@@ -26,19 +40,7 @@ type ApprovalMapEntry = {
|
|
|
26
40
|
readonly decision: "deny";
|
|
27
41
|
readonly reason?: string;
|
|
28
42
|
};
|
|
29
|
-
declare const fromApprovalMap: (predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) =>
|
|
30
|
-
/**
|
|
31
|
-
* Authz gate. `canApprove` runs BEFORE the inner resolver; failures
|
|
32
|
-
* short-circuit to a `permission_denied` rejection. Override `onForbidden`
|
|
33
|
-
* if your audit format wants a different kind or reason.
|
|
34
|
-
*/
|
|
35
|
-
declare const withPermissions: (inner: Resolver, canApprove: (call: FunctionCall) => Effect.Effect<boolean>, onForbidden?: (call: FunctionCall) => ToolResult) => Resolver;
|
|
36
|
-
/**
|
|
37
|
-
* Fallback gate. If `inner` returns a Reject whose result matches the
|
|
38
|
-
* `recoverable` predicate, run `fallback(call)` instead and use that
|
|
39
|
-
* decision. Otherwise pass the original Reject through.
|
|
40
|
-
*/
|
|
41
|
-
declare const withFallback: (inner: Resolver, recoverable: (result: ToolResult) => boolean, fallback: (call: FunctionCall) => Effect.Effect<ToolDecision>) => Resolver;
|
|
43
|
+
declare const fromApprovalMap: (predicate: (call: FunctionCall) => boolean, approvals: ReadonlyMap<string, ApprovalMapEntry>) => (calls: ReadonlyArray<FunctionCall>) => ToolCallPlan;
|
|
42
44
|
//#endregion
|
|
43
|
-
export { ApprovalMapEntry, Verdict, fromApprovalMap, fromVerdictQueue,
|
|
45
|
+
export { ApprovalMapEntry, ToolCallDecision, ToolCallPlan, Verdict, approvalRequested, approve, fromApprovalMap, fromVerdictQueue, reject, splitToolCallDecisions };
|
|
44
46
|
//# sourceMappingURL=Resolvers.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Resolvers.d.mts","names":[],"sources":["../../src/tool/Resolvers.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"Resolvers.d.mts","names":[],"sources":["../../src/tool/Resolvers.ts"],"mappings":";;;;;;UAYiB,YAAA;EAAA,SACN,QAAA,EAAU,aAAA,CAAc,YAAA;EAAA,SACxB,QAAA,EAAU,aAAA,CAAc,UAAA;AAAA;AAAA,KAGvB,gBAAA;EAAA,SACG,IAAA;EAAA,SAA2B,IAAA,EAAM,YAAA;AAAA;EAAA,SACjC,IAAA;EAAA,SAA2B,MAAA,EAAQ,UAAA;AAAA;AAAA,cAErC,OAAA,GAAW,IAAA,EAAM,YAAA,KAAe,gBAAA;AAAA,cAKhC,MAAA,GAAU,MAAA,EAAQ,UAAA,KAAa,gBAAA;AAAA,cAK/B,sBAAA,GACX,SAAA,EAAW,aAAA,CAAc,gBAAA,MACxB,YAAA;AAAA,cASU,iBAAA,GAAqB,IAAA,EAAM,YAAA,KAAe,SAAA;AAAA,UAWtC,OAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;;;;;AAnCX;cA2Ca,gBAAA,GAET,SAAA,GAAY,IAAA,EAAM,YAAA,cAClB,QAAA,EAAU,KAAA,CAAM,OAAA,CAAQ,OAAA,OAGxB,KAAA,EAAO,aAAA,CAAc,YAAA,MACpB,MAAA,CAAO,MAAA;EAAA,SAEG,QAAA,EAAU,aAAA,CAAc,YAAA;EAAA,SACxB,SAAA,EAAW,MAAA,CAAO,MAAA,CAAO,gBAAA;EAAA,SACzB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,UAGnC,KAAA,CAAM,KAAA;AAAA,KAkDE,gBAAA;EAAA,SACG,QAAA;AAAA;EAAA,SACA,QAAA;EAAA,SAA2B,MAAA;AAAA;AAAA,cAE7B,eAAA,GAET,SAAA,GAAY,IAAA,EAAM,YAAA,cAClB,SAAA,EAAW,WAAA,SAAoB,gBAAA,OAEhC,KAAA,EAAO,aAAA,CAAc,YAAA,MAAgB,YAAA"}
|
package/dist/tool/Resolvers.mjs
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
|
-
import { cancelled, denied
|
|
1
|
+
import { cancelled, denied } from "./Outcome.mjs";
|
|
2
2
|
import { Deferred, Effect, Queue, Stream } from "effect";
|
|
3
3
|
//#region src/tool/Resolvers.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* for layering policy on top.
|
|
5
|
+
* Approval helpers for the two transport flavors.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - `withFallback` : recovery wrapper.
|
|
12
|
-
*
|
|
13
|
-
* None of these know about the executor's stream shape; they just produce
|
|
14
|
-
* `Effect<ToolDecision>`s a `Resolver` can return.
|
|
7
|
+
* These helpers only decide which calls are approved and which synthetic
|
|
8
|
+
* results must be returned to the model. Tool execution stays explicit at
|
|
9
|
+
* the recipe boundary via `Toolkit.executeAll`.
|
|
15
10
|
*/
|
|
11
|
+
const approve = (call) => ({
|
|
12
|
+
_tag: "Approved",
|
|
13
|
+
call
|
|
14
|
+
});
|
|
15
|
+
const reject = (result) => ({
|
|
16
|
+
_tag: "Rejected",
|
|
17
|
+
result
|
|
18
|
+
});
|
|
19
|
+
const splitToolCallDecisions = (decisions) => decisions.reduce((acc, decision) => decision._tag === "Approved" ? {
|
|
20
|
+
...acc,
|
|
21
|
+
approved: [...acc.approved, decision.call]
|
|
22
|
+
} : {
|
|
23
|
+
...acc,
|
|
24
|
+
rejected: [...acc.rejected, decision.result]
|
|
25
|
+
}, {
|
|
26
|
+
approved: [],
|
|
27
|
+
rejected: []
|
|
28
|
+
});
|
|
29
|
+
const approvalRequested = (call) => ({
|
|
30
|
+
_tag: "ApprovalRequested",
|
|
31
|
+
call_id: call.call_id,
|
|
32
|
+
tool: call.name,
|
|
33
|
+
arguments: call.arguments
|
|
34
|
+
});
|
|
16
35
|
/**
|
|
17
|
-
* Queue-backed
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* merges the announce stream into its consumer view.
|
|
36
|
+
* Queue-backed approval planner. Safe calls are returned immediately in
|
|
37
|
+
* `approved`; gated calls emit `ApprovalRequested` events and later produce
|
|
38
|
+
* one `ToolCallDecision` when their matching verdict arrives.
|
|
21
39
|
*/
|
|
22
40
|
const fromVerdictQueue = (predicate, verdicts) => (calls) => Effect.gen(function* () {
|
|
23
41
|
const gated = calls.filter(predicate);
|
|
42
|
+
const approved = calls.filter((call) => !predicate(call));
|
|
24
43
|
const entries = yield* Effect.forEach(gated, (call) => Deferred.make().pipe(Effect.map((d) => [call.call_id, d])));
|
|
25
44
|
const deferreds = new Map(entries);
|
|
26
45
|
yield* Effect.forkScoped(Effect.forever(Effect.gen(function* () {
|
|
@@ -28,40 +47,22 @@ const fromVerdictQueue = (predicate, verdicts) => (calls) => Effect.gen(function
|
|
|
28
47
|
const d = deferreds.get(v.call_id);
|
|
29
48
|
if (d !== void 0) yield* Deferred.succeed(d, v);
|
|
30
49
|
})));
|
|
31
|
-
const resolve = (call) => {
|
|
32
|
-
if (!predicate(call)) return Effect.succeed(execute);
|
|
33
|
-
const d = deferreds.get(call.call_id);
|
|
34
|
-
return Deferred.await(d).pipe(Effect.map((v) => v.decision === "approve" ? execute : reject(denied(call, v.reason))));
|
|
35
|
-
};
|
|
36
50
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})))
|
|
51
|
+
approved,
|
|
52
|
+
decisions: Stream.fromIterable(gated).pipe(Stream.flatMap((call) => {
|
|
53
|
+
const d = deferreds.get(call.call_id);
|
|
54
|
+
return Stream.fromEffect(Deferred.await(d).pipe(Effect.map((v) => v.decision === "approve" ? approve(call) : reject(denied(call, v.reason)))));
|
|
55
|
+
}, { concurrency: "unbounded" })),
|
|
56
|
+
announce: Stream.fromIterable(gated.map(approvalRequested))
|
|
44
57
|
};
|
|
45
58
|
});
|
|
46
|
-
const fromApprovalMap = (predicate, approvals) => (call) => {
|
|
47
|
-
if (!predicate(call)) return
|
|
59
|
+
const fromApprovalMap = (predicate, approvals) => (calls) => splitToolCallDecisions(calls.map((call) => {
|
|
60
|
+
if (!predicate(call)) return approve(call);
|
|
48
61
|
const v = approvals.get(call.call_id);
|
|
49
|
-
if (v === void 0) return
|
|
50
|
-
return
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Authz gate. `canApprove` runs BEFORE the inner resolver; failures
|
|
54
|
-
* short-circuit to a `permission_denied` rejection. Override `onForbidden`
|
|
55
|
-
* if your audit format wants a different kind or reason.
|
|
56
|
-
*/
|
|
57
|
-
const withPermissions = (inner, canApprove, onForbidden = (call) => rejected(call, "permission_denied", "missing permissions")) => (call) => canApprove(call).pipe(Effect.flatMap((allowed) => allowed ? inner(call) : Effect.succeed(reject(onForbidden(call)))));
|
|
58
|
-
/**
|
|
59
|
-
* Fallback gate. If `inner` returns a Reject whose result matches the
|
|
60
|
-
* `recoverable` predicate, run `fallback(call)` instead and use that
|
|
61
|
-
* decision. Otherwise pass the original Reject through.
|
|
62
|
-
*/
|
|
63
|
-
const withFallback = (inner, recoverable, fallback) => (call) => inner(call).pipe(Effect.flatMap((decision) => decision._tag === "Reject" && recoverable(decision.result) ? fallback(call) : Effect.succeed(decision)));
|
|
62
|
+
if (v === void 0) return reject(cancelled(call));
|
|
63
|
+
return v.decision === "approve" ? approve(call) : reject(denied(call, v.reason));
|
|
64
|
+
}));
|
|
64
65
|
//#endregion
|
|
65
|
-
export { fromApprovalMap, fromVerdictQueue,
|
|
66
|
+
export { approvalRequested, approve, fromApprovalMap, fromVerdictQueue, reject, splitToolCallDecisions };
|
|
66
67
|
|
|
67
68
|
//# sourceMappingURL=Resolvers.mjs.map
|