@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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/{Outcome-C2JYknCu.d.mts → Outcome-GiaNvt7i.d.mts} +2 -10
  3. package/dist/Outcome-GiaNvt7i.d.mts.map +1 -0
  4. package/dist/{ToolEvent-B2N10hr3.d.mts → ToolEvent-wTMgb2GO.d.mts} +2 -2
  5. package/dist/{ToolEvent-B2N10hr3.d.mts.map → ToolEvent-wTMgb2GO.d.mts.map} +1 -1
  6. package/dist/{Turn-rlTfuHaQ.d.mts → Turn-Bi83du4I.d.mts} +8 -19
  7. package/dist/{Turn-rlTfuHaQ.d.mts.map → Turn-Bi83du4I.d.mts.map} +1 -1
  8. package/dist/domain/Turn.d.mts +2 -2
  9. package/dist/domain/Turn.mjs +11 -7
  10. package/dist/domain/Turn.mjs.map +1 -1
  11. package/dist/index.d.mts +2 -2
  12. package/dist/language-model/LanguageModel.d.mts +1 -1
  13. package/dist/loop/Loop.d.mts +87 -2
  14. package/dist/loop/Loop.d.mts.map +1 -0
  15. package/dist/testing/MockProvider.d.mts +1 -1
  16. package/dist/tool/HistoryCheck.d.mts +1 -1
  17. package/dist/tool/Outcome.d.mts +2 -2
  18. package/dist/tool/Outcome.mjs +4 -10
  19. package/dist/tool/Outcome.mjs.map +1 -1
  20. package/dist/tool/Resolvers.d.mts +24 -22
  21. package/dist/tool/Resolvers.d.mts.map +1 -1
  22. package/dist/tool/Resolvers.mjs +45 -44
  23. package/dist/tool/Resolvers.mjs.map +1 -1
  24. package/dist/tool/ToolEvent.d.mts +1 -1
  25. package/dist/tool/ToolEvent.mjs.map +1 -1
  26. package/dist/tool/Toolkit.d.mts +9 -9
  27. package/dist/tool/Toolkit.d.mts.map +1 -1
  28. package/dist/tool/Toolkit.mjs +11 -10
  29. package/dist/tool/Toolkit.mjs.map +1 -1
  30. package/package.json +1 -1
  31. package/src/domain/Turn.ts +7 -17
  32. package/src/tool/Outcome.ts +3 -17
  33. package/src/tool/Resolvers.test.ts +39 -86
  34. package/src/tool/Resolvers.ts +74 -93
  35. package/src/tool/ToolEvent.ts +2 -2
  36. package/src/tool/Toolkit.ts +13 -39
  37. package/dist/Loop-CzSJo1h8.d.mts +0 -87
  38. package/dist/Loop-CzSJo1h8.d.mts.map +0 -1
  39. package/dist/Outcome-C2JYknCu.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Resolvers.mjs","names":[],"sources":["../../src/tool/Resolvers.ts"],"sourcesContent":["/**\n * Ready-made `Resolver`s for the two transport flavors plus combinators\n * for layering policy on top.\n *\n * - `fromVerdictQueue` : long-lived channel (WebSocket / SSE).\n * - `fromApprovalMap` : request-shaped (HTTP chat).\n * - `withPermissions` : authz wrapper.\n * - `withFallback` : recovery wrapper.\n *\n * None of these know about the executor's stream shape; they just produce\n * `Effect<ToolDecision>`s a `Resolver` can return.\n */\nimport { Deferred, Effect, Queue, Scope, Stream } from \"effect\"\nimport type { FunctionCall } from \"../domain/Items.js\"\nimport {\n type ToolDecision,\n type ToolResult,\n cancelled,\n denied,\n execute,\n reject,\n rejected,\n} from \"./Outcome.js\"\nimport type { Resolver } from \"./Toolkit.js\"\nimport type { ToolEvent } from \"./ToolEvent.js\"\n\n// ---------------------------------------------------------------------------\n// Verdict queue (WebSocket-style transport).\n// ---------------------------------------------------------------------------\n\nexport interface Verdict {\n readonly call_id: string\n readonly decision: \"approve\" | \"deny\"\n readonly reason?: string\n}\n\n/**\n * Queue-backed resolver. The router fiber drains verdicts and resolves\n * pre-registered Deferreds keyed by `call_id`. Returns the resolver and\n * a stream of `ApprovalRequested` events for the gated calls; the recipe\n * merges the announce stream into its consumer view.\n */\nexport const fromVerdictQueue =\n (\n predicate: (call: FunctionCall) => boolean,\n verdicts: Queue.Dequeue<Verdict>,\n ) =>\n (\n calls: ReadonlyArray<FunctionCall>,\n ): Effect.Effect<\n {\n readonly resolve: Resolver\n readonly announce: Stream.Stream<ToolEvent>\n },\n never,\n Scope.Scope\n > =>\n Effect.gen(function* () {\n const gated = calls.filter(predicate)\n\n const entries = yield* Effect.forEach(gated, (call) =>\n Deferred.make<Verdict>().pipe(Effect.map((d) => [call.call_id, d] as const)),\n )\n const deferreds: ReadonlyMap<string, Deferred.Deferred<Verdict>> = new Map(entries)\n\n // Router is forked into the surrounding Scope so it lives as long\n // as the consumer is pulling events. Recipes typically supply the\n // scope by wrapping the events construction in `Stream.unwrap`.\n yield* Effect.forkScoped(\n Effect.forever(\n Effect.gen(function* () {\n const v = yield* Queue.take(verdicts)\n const d = deferreds.get(v.call_id)\n if (d !== undefined) yield* Deferred.succeed(d, v)\n }),\n ),\n )\n\n const resolve: Resolver = (call) => {\n if (!predicate(call)) return Effect.succeed(execute)\n const d = deferreds.get(call.call_id)!\n return Deferred.await(d).pipe(\n Effect.map((v) =>\n v.decision === \"approve\" ? execute : reject(denied(call, v.reason)),\n ),\n )\n }\n\n const announce = Stream.fromIterable<ToolEvent>(\n gated.map((call) => ({\n _tag: \"ApprovalRequested\",\n call_id: call.call_id,\n tool: call.name,\n arguments: call.arguments,\n })),\n )\n\n return { resolve, announce }\n })\n\n// ---------------------------------------------------------------------------\n// Approval map (HTTP-style transport). Verdicts arrive synchronously\n// bundled in the request payload. Missing entries → cancelled.\n// ---------------------------------------------------------------------------\n\nexport type ApprovalMapEntry =\n | { readonly decision: \"approve\" }\n | { readonly decision: \"deny\"; readonly reason?: string }\n\nexport const fromApprovalMap =\n (\n predicate: (call: FunctionCall) => boolean,\n approvals: ReadonlyMap<string, ApprovalMapEntry>,\n ): Resolver =>\n (call) => {\n if (!predicate(call)) return Effect.succeed(execute)\n const v = approvals.get(call.call_id)\n if (v === undefined) return Effect.succeed(reject(cancelled(call)))\n return Effect.succeed(\n v.decision === \"approve\" ? execute : reject(denied(call, v.reason)),\n )\n }\n\n// ---------------------------------------------------------------------------\n// Combinators - compose policy onto an inner resolver.\n// ---------------------------------------------------------------------------\n\n/**\n * Authz gate. `canApprove` runs BEFORE the inner resolver; failures\n * short-circuit to a `permission_denied` rejection. Override `onForbidden`\n * if your audit format wants a different kind or reason.\n */\nexport const withPermissions =\n (\n inner: Resolver,\n canApprove: (call: FunctionCall) => Effect.Effect<boolean>,\n onForbidden: (call: FunctionCall) => ToolResult = (call) =>\n rejected(call, \"permission_denied\", \"missing permissions\"),\n ): Resolver =>\n (call) =>\n canApprove(call).pipe(\n Effect.flatMap((allowed) =>\n allowed ? inner(call) : Effect.succeed(reject(onForbidden(call))),\n ),\n )\n\n/**\n * Fallback gate. If `inner` returns a Reject whose result matches the\n * `recoverable` predicate, run `fallback(call)` instead and use that\n * decision. Otherwise pass the original Reject through.\n */\nexport const withFallback =\n (\n inner: Resolver,\n recoverable: (result: ToolResult) => boolean,\n fallback: (call: FunctionCall) => Effect.Effect<ToolDecision>,\n ): Resolver =>\n (call) =>\n inner(call).pipe(\n Effect.flatMap((decision) =>\n decision._tag === \"Reject\" && recoverable(decision.result)\n ? fallback(call)\n : Effect.succeed(decision),\n ),\n )\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,oBAET,WACA,cAGA,UASA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,MAAM,OAAO,UAAU;CAErC,MAAM,UAAU,OAAO,OAAO,QAAQ,QAAQ,SAC5C,SAAS,MAAe,CAAC,KAAK,OAAO,KAAK,MAAM,CAAC,KAAK,SAAS,EAAE,CAAU,CAAC,CAC7E;CACD,MAAM,YAA6D,IAAI,IAAI,QAAQ;AAKnF,QAAO,OAAO,WACZ,OAAO,QACL,OAAO,IAAI,aAAa;EACtB,MAAM,IAAI,OAAO,MAAM,KAAK,SAAS;EACrC,MAAM,IAAI,UAAU,IAAI,EAAE,QAAQ;AAClC,MAAI,MAAM,KAAA,EAAW,QAAO,SAAS,QAAQ,GAAG,EAAE;GAClD,CACH,CACF;CAED,MAAM,WAAqB,SAAS;AAClC,MAAI,CAAC,UAAU,KAAK,CAAE,QAAO,OAAO,QAAQ,QAAQ;EACpD,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,SAAO,SAAS,MAAM,EAAE,CAAC,KACvB,OAAO,KAAK,MACV,EAAE,aAAa,YAAY,UAAU,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC,CACpE,CACF;;AAYH,QAAO;EAAE;EAAS,UATD,OAAO,aACtB,MAAM,KAAK,UAAU;GACnB,MAAM;GACN,SAAS,KAAK;GACd,MAAM,KAAK;GACX,WAAW,KAAK;GACjB,EAAE,CAGqB;EAAE;EAC5B;AAWN,MAAa,mBAET,WACA,eAED,SAAS;AACR,KAAI,CAAC,UAAU,KAAK,CAAE,QAAO,OAAO,QAAQ,QAAQ;CACpD,MAAM,IAAI,UAAU,IAAI,KAAK,QAAQ;AACrC,KAAI,MAAM,KAAA,EAAW,QAAO,OAAO,QAAQ,OAAO,UAAU,KAAK,CAAC,CAAC;AACnE,QAAO,OAAO,QACZ,EAAE,aAAa,YAAY,UAAU,OAAO,OAAO,MAAM,EAAE,OAAO,CAAC,CACpE;;;;;;;AAYL,MAAa,mBAET,OACA,YACA,eAAmD,SACjD,SAAS,MAAM,qBAAqB,sBAAsB,MAE7D,SACC,WAAW,KAAK,CAAC,KACf,OAAO,SAAS,YACd,UAAU,MAAM,KAAK,GAAG,OAAO,QAAQ,OAAO,YAAY,KAAK,CAAC,CAAC,CAClE,CACF;;;;;;AAOL,MAAa,gBAET,OACA,aACA,cAED,SACC,MAAM,KAAK,CAAC,KACV,OAAO,SAAS,aACd,SAAS,SAAS,YAAY,YAAY,SAAS,OAAO,GACtD,SAAS,KAAK,GACd,OAAO,QAAQ,SAAS,CAC7B,CACF"}
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 interface 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 = (\n decisions: ReadonlyArray<ToolCallDecision>,\n): 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 interface 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 (\n predicate: (call: FunctionCall) => boolean,\n verdicts: Queue.Dequeue<Verdict>,\n ) =>\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 (\n predicate: (call: FunctionCall) => boolean,\n approvals: ReadonlyMap<string, ApprovalMapEntry>,\n ) =>\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\n"],"mappings":";;;;;;;;;;AAqBA,MAAa,WAAW,UAA0C;CAChE,MAAM;CACN;CACD;AAED,MAAa,UAAU,YAA0C;CAC/D,MAAM;CACN;CACD;AAED,MAAa,0BACX,cAEA,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,oBAET,WACA,cAGA,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,mBAET,WACA,eAED,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"}
@@ -1,2 +1,2 @@
1
- import { i as isOutput, n as isApprovalRequested, r as isIntermediate, t as ToolEvent } from "../ToolEvent-B2N10hr3.mjs";
1
+ import { i as isOutput, n as isApprovalRequested, r as isIntermediate, t as ToolEvent } from "../ToolEvent-wTMgb2GO.mjs";
2
2
  export { ToolEvent, isApprovalRequested, isIntermediate, isOutput };
@@ -1 +1 @@
1
- {"version":3,"file":"ToolEvent.mjs","names":[],"sources":["../../src/tool/ToolEvent.ts"],"sourcesContent":["/**\n * The event type emitted by `Toolkit.executeAllWithResolver`.\n *\n * - ApprovalRequested : gated calls before resolver returns\n * - Intermediate : per-element passthrough from a streaming tool's run\n * - Output : terminal result (carries a structured ToolResult)\n *\n * Recipes thread `ToolEvent.Output.result` through `nextStateFrom` and apply\n * `toFunctionCallOutput` when appending to history.\n */\nimport type { ToolResult } from \"./Outcome.js\"\n\nexport type ToolEvent =\n | {\n readonly _tag: \"ApprovalRequested\"\n readonly call_id: string\n readonly tool: string\n readonly arguments: string\n }\n | {\n readonly _tag: \"Intermediate\"\n readonly call_id: string\n readonly tool: string\n readonly data: unknown\n }\n | { readonly _tag: \"Output\"; readonly result: ToolResult }\n\nexport const isApprovalRequested = (\n e: ToolEvent,\n): e is Extract<ToolEvent, { _tag: \"ApprovalRequested\" }> => e._tag === \"ApprovalRequested\"\n\nexport const isIntermediate = (\n e: ToolEvent,\n): e is Extract<ToolEvent, { _tag: \"Intermediate\" }> => e._tag === \"Intermediate\"\n\nexport const isOutput = (e: ToolEvent): e is Extract<ToolEvent, { _tag: \"Output\" }> =>\n e._tag === \"Output\"\n"],"mappings":";AA2BA,MAAa,uBACX,MAC2D,EAAE,SAAS;AAExE,MAAa,kBACX,MACsD,EAAE,SAAS;AAEnE,MAAa,YAAY,MACvB,EAAE,SAAS"}
1
+ {"version":3,"file":"ToolEvent.mjs","names":[],"sources":["../../src/tool/ToolEvent.ts"],"sourcesContent":["/**\n * The event type emitted while handling tool calls.\n *\n * - ApprovalRequested : gated calls waiting for approval\n * - Intermediate : per-element passthrough from a streaming tool's run\n * - Output : terminal result (carries a structured ToolResult)\n *\n * Recipes thread `ToolEvent.Output.result` through `nextStateFrom` and apply\n * `toFunctionCallOutput` when appending to history.\n */\nimport type { ToolResult } from \"./Outcome.js\"\n\nexport type ToolEvent =\n | {\n readonly _tag: \"ApprovalRequested\"\n readonly call_id: string\n readonly tool: string\n readonly arguments: string\n }\n | {\n readonly _tag: \"Intermediate\"\n readonly call_id: string\n readonly tool: string\n readonly data: unknown\n }\n | { readonly _tag: \"Output\"; readonly result: ToolResult }\n\nexport const isApprovalRequested = (\n e: ToolEvent,\n): e is Extract<ToolEvent, { _tag: \"ApprovalRequested\" }> => e._tag === \"ApprovalRequested\"\n\nexport const isIntermediate = (\n e: ToolEvent,\n): e is Extract<ToolEvent, { _tag: \"Intermediate\" }> => e._tag === \"Intermediate\"\n\nexport const isOutput = (e: ToolEvent): e is Extract<ToolEvent, { _tag: \"Output\" }> =>\n e._tag === \"Output\"\n"],"mappings":";AA2BA,MAAa,uBACX,MAC2D,EAAE,SAAS;AAExE,MAAa,kBACX,MACsD,EAAE,SAAS;AAEnE,MAAa,YAAY,MACvB,EAAE,SAAS"}
@@ -1,13 +1,13 @@
1
1
  import { o as FunctionCall } from "../Items-D1C2686t.mjs";
2
2
  import { a as Tool, o as ToolDescriptor, t as AnyKindTool } from "../Tool-5wxOCuOh.mjs";
3
- import { t as Event } from "../Loop-CzSJo1h8.mjs";
4
- import { n as ToolResult, t as ToolDecision } from "../Outcome-C2JYknCu.mjs";
5
- import { t as ToolEvent } from "../ToolEvent-B2N10hr3.mjs";
6
- import { Effect, Stream } from "effect";
3
+ import { Event } from "../loop/Loop.mjs";
4
+ import { t as ToolResult } from "../Outcome-GiaNvt7i.mjs";
5
+ import { t as ToolEvent } from "../ToolEvent-wTMgb2GO.mjs";
6
+ import { Stream } from "effect";
7
7
 
8
8
  //#region src/tool/Toolkit.d.ts
9
9
  declare namespace Toolkit_d_exports {
10
- export { AnyTool, ExecuteOptions, Resolver, Toolkit, ToolsR, executeAll, executeAllWithResolver, make, nextStateFrom, toDescriptors };
10
+ export { AnyTool, ExecuteOptions, Toolkit, ToolsR, executeAll, make, nextStateFrom, outputEvent, outputEvents, toDescriptors };
11
11
  }
12
12
  type AnyTool = Tool<string, any, any, any>;
13
13
  type Toolkit<Tools extends ReadonlyArray<AnyTool>> = {
@@ -21,14 +21,14 @@ declare const make: <const Tools extends ReadonlyArray<AnyTool>>(tools: Tools) =
21
21
  * Standard Schema converter (draft 2020-12).
22
22
  */
23
23
  declare const toDescriptors: <Tools extends ReadonlyArray<AnyTool>>(toolkit: Toolkit<Tools>) => ReadonlyArray<ToolDescriptor>;
24
- type Resolver = (call: FunctionCall) => Effect.Effect<ToolDecision>;
25
24
  interface ExecuteOptions {
26
25
  readonly concurrency?: number | "unbounded";
27
26
  }
28
- declare const executeAllWithResolver: (tools: ReadonlyArray<AnyKindTool>, calls: ReadonlyArray<FunctionCall>, resolve: Resolver, options?: ExecuteOptions) => Stream.Stream<ToolEvent>;
29
- /** No-resolver shortcut: every call gets `Execute`. */
27
+ /** Execute every provided call. Approval/rejection policy belongs upstream. */
30
28
  declare const executeAll: (tools: ReadonlyArray<AnyKindTool>, calls: ReadonlyArray<FunctionCall>, options?: ExecuteOptions) => Stream.Stream<ToolEvent>;
29
+ declare const outputEvent: (result: ToolResult) => ToolEvent;
30
+ declare const outputEvents: (results: ReadonlyArray<ToolResult>) => Stream.Stream<ToolEvent>;
31
31
  declare const nextStateFrom: <S>(stream: Stream.Stream<ToolEvent>, build: (results: ReadonlyArray<ToolResult>) => S) => Stream.Stream<Event<ToolEvent, S>>;
32
32
  //#endregion
33
- export { AnyTool, ExecuteOptions, Resolver, Toolkit, ToolsR, executeAll, executeAllWithResolver, make, nextStateFrom, Toolkit_d_exports as t, toDescriptors };
33
+ export { AnyTool, ExecuteOptions, Toolkit, ToolsR, executeAll, make, nextStateFrom, outputEvent, outputEvents, Toolkit_d_exports as t, toDescriptors };
34
34
  //# sourceMappingURL=Toolkit.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Toolkit.d.mts","names":[],"sources":["../../src/tool/Toolkit.ts"],"mappings":";;;;;;;;;;;KAqBY,OAAA,GAAU,IAAA;AAAA,KAEV,OAAA,eAAsB,aAAA,CAAc,OAAA;EAAA,SACrC,KAAA,EAAO,KAAA;AAAA;AAAA,KAGN,MAAA,eAAqB,aAAA,CAAc,OAAA,KAC7C,KAAA,iBAAsB,IAAA,2BAA+B,CAAA;AAAA,cAE1C,IAAA,uBAA4B,aAAA,CAAc,OAAA,GAAU,KAAA,EAAO,KAAA,KAAQ,OAAA,CAAQ,KAAA;;;;;;cAS3E,aAAA,iBAA+B,aAAA,CAAc,OAAA,GACxD,OAAA,EAAS,OAAA,CAAQ,KAAA,MAChB,aAAA,CAAc,cAAA;AAAA,KAmBL,QAAA,IAAY,IAAA,EAAM,YAAA,KAAiB,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA,UAE5C,cAAA;EAAA,SACN,WAAA;AAAA;AAAA,cAGE,sBAAA,GACX,KAAA,EAAO,aAAA,CAAc,WAAA,GACrB,KAAA,EAAO,aAAA,CAAc,YAAA,GACrB,OAAA,EAAS,QAAA,EACT,OAAA,GAAU,cAAA,KACT,MAAA,CAAO,MAAA,CAAO,SAAA;;cAYJ,UAAA,GACX,KAAA,EAAO,aAAA,CAAc,WAAA,GACrB,KAAA,EAAO,aAAA,CAAc,YAAA,GACrB,OAAA,GAAU,cAAA,KACT,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,cAmIJ,aAAA,MACX,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,SAAA,GACtB,KAAA,GAAQ,OAAA,EAAS,aAAA,CAAc,UAAA,MAAgB,CAAA,KAC9C,MAAA,CAAO,MAAA,CAAO,KAAA,CAAW,SAAA,EAAW,CAAA"}
1
+ {"version":3,"file":"Toolkit.d.mts","names":[],"sources":["../../src/tool/Toolkit.ts"],"mappings":";;;;;;;;;;;KAmBY,OAAA,GAAU,IAAA;AAAA,KAEV,OAAA,eAAsB,aAAA,CAAc,OAAA;EAAA,SACrC,KAAA,EAAO,KAAA;AAAA;AAAA,KAGN,MAAA,eAAqB,aAAA,CAAc,OAAA,KAC7C,KAAA,iBAAsB,IAAA,2BAA+B,CAAA;AAAA,cAE1C,IAAA,uBAA4B,aAAA,CAAc,OAAA,GAAU,KAAA,EAAO,KAAA,KAAQ,OAAA,CAAQ,KAAA;;;;;;cAS3E,aAAA,iBAA+B,aAAA,CAAc,OAAA,GACxD,OAAA,EAAS,OAAA,CAAQ,KAAA,MAChB,aAAA,CAAc,cAAA;AAAA,UAgBA,cAAA;EAAA,SACN,WAAA;AAAA;AArCX;AAAA,cAyCa,UAAA,GACX,KAAA,EAAO,aAAA,CAAc,WAAA,GACrB,KAAA,EAAO,aAAA,CAAc,YAAA,GACrB,OAAA,GAAU,cAAA,KACT,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,cAOJ,WAAA,GAAe,MAAA,EAAQ,UAAA,KAAa,SAAA;AAAA,cAEpC,YAAA,GACX,OAAA,EAAS,aAAA,CAAc,UAAA,MACtB,MAAA,CAAO,MAAA,CAAO,SAAA;AAAA,cAqHJ,aAAA,MACX,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,SAAA,GACtB,KAAA,GAAQ,OAAA,EAAS,aAAA,CAAc,UAAA,MAAgB,CAAA,KAC9C,MAAA,CAAO,MAAA,CAAO,KAAA,CAAW,SAAA,EAAW,CAAA"}
@@ -1,15 +1,16 @@
1
1
  import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
2
2
  import { nextAfterFold } from "../loop/Loop.mjs";
3
3
  import { isStreamingTool } from "./Tool.mjs";
4
- import { execute, executionError, rejected } from "./Outcome.mjs";
4
+ import { executionError, rejected } from "./Outcome.mjs";
5
5
  import { isOutput } from "./ToolEvent.mjs";
6
- import { Array, Effect, Match, Ref, Stream } from "effect";
6
+ import { Array, Effect, Ref, Stream } from "effect";
7
7
  //#region src/tool/Toolkit.ts
8
8
  var Toolkit_exports = /* @__PURE__ */ __exportAll({
9
9
  executeAll: () => executeAll,
10
- executeAllWithResolver: () => executeAllWithResolver,
11
10
  make: () => make,
12
11
  nextStateFrom: () => nextStateFrom,
12
+ outputEvent: () => outputEvent,
13
+ outputEvents: () => outputEvents,
13
14
  toDescriptors: () => toDescriptors
14
15
  });
15
16
  const make = (tools) => ({ tools });
@@ -31,13 +32,13 @@ const toDescriptors = (toolkit) => toolkit.tools.map((tool) => {
31
32
  inputSchema
32
33
  };
33
34
  });
34
- const executeAllWithResolver = (tools, calls, resolve, options) => Stream.fromIterable(calls).pipe(Stream.flatMap((call) => Stream.unwrap(resolve(call).pipe(Effect.map((decision) => dispatch(tools, call, decision)))), { concurrency: options?.concurrency ?? "unbounded" }));
35
- /** No-resolver shortcut: every call gets `Execute`. */
36
- const executeAll = (tools, calls, options) => executeAllWithResolver(tools, calls, () => Effect.succeed(execute), options);
37
- const dispatch = (tools, call, decision) => Match.value(decision).pipe(Match.tag("Execute", () => runOne(tools, call)), Match.tag("Reject", (d) => Stream.succeed({
35
+ /** Execute every provided call. Approval/rejection policy belongs upstream. */
36
+ const executeAll = (tools, calls, options) => Stream.fromIterable(calls).pipe(Stream.flatMap((call) => runOne(tools, call), { concurrency: options?.concurrency ?? "unbounded" }));
37
+ const outputEvent = (result) => ({
38
38
  _tag: "Output",
39
- result: d.result
40
- })), Match.exhaustive);
39
+ result
40
+ });
41
+ const outputEvents = (results) => Stream.fromIterable(results.map(outputEvent));
41
42
  const valueResult = (call, tool, value) => ({
42
43
  _tag: "Value",
43
44
  call_id: call.call_id,
@@ -100,6 +101,6 @@ const runStreaming = (tool, call) => Stream.unwrap(Effect.gen(function* () {
100
101
  })));
101
102
  const nextStateFrom = (stream, build) => nextAfterFold(stream, [], (acc, e) => isOutput(e) ? Array.append(acc, e.result) : acc, build);
102
103
  //#endregion
103
- export { executeAll, executeAllWithResolver, make, nextStateFrom, Toolkit_exports as t, toDescriptors };
104
+ export { executeAll, make, nextStateFrom, outputEvent, outputEvents, Toolkit_exports as t, toDescriptors };
104
105
 
105
106
  //# sourceMappingURL=Toolkit.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Toolkit.mjs","names":["executeDecision","Arr","Loop.nextAfterFold"],"sources":["../../src/tool/Toolkit.ts"],"sourcesContent":["import { Array as Arr, Effect, Match, Ref, Stream } from \"effect\"\nimport * as Loop from \"../loop/Loop.js\"\nimport type { FunctionCall } from \"../domain/Items.js\"\nimport {\n type AnyKindTool,\n type AnyPlainTool,\n type AnyStreamingTool,\n isStreamingTool,\n type Tool,\n type ToolDescriptor,\n} from \"./Tool.js\"\nimport {\n type ToolDecision,\n type ToolResult,\n execute as executeDecision,\n executionError,\n rejected,\n} from \"./Outcome.js\"\nimport type { ToolEvent } from \"./ToolEvent.js\"\nimport { isOutput } from \"./ToolEvent.js\"\n\nexport type AnyTool = Tool<string, any, any, any>\n\nexport type Toolkit<Tools extends ReadonlyArray<AnyTool>> = {\n readonly tools: Tools\n}\n\nexport type ToolsR<Tools extends ReadonlyArray<AnyTool>> =\n Tools[number] extends Tool<any, any, any, infer R> ? R : never\n\nexport const make = <const Tools extends ReadonlyArray<AnyTool>>(tools: Tools): Toolkit<Tools> => ({\n tools,\n})\n\n/**\n * Render every tool in a toolkit to a provider-agnostic descriptor.\n * `inputSchema` is the JSON Schema document produced by the tool's\n * Standard Schema converter (draft 2020-12).\n */\nexport const toDescriptors = <Tools extends ReadonlyArray<AnyTool>>(\n toolkit: Toolkit<Tools>,\n): ReadonlyArray<ToolDescriptor> =>\n toolkit.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\n// ---------------------------------------------------------------------------\n// Resolver-based executor. Streams `ToolEvent`s in real time, dispatches\n// streaming and plain tools uniformly, and lets the caller decide what\n// happens to each call (Execute or Reject) before execution.\n//\n// `executeAllWithResolver` is the general primitive. `executeAllStream` is\n// the no-resolver shortcut.\n// ---------------------------------------------------------------------------\n\nexport type Resolver = (call: FunctionCall) => Effect.Effect<ToolDecision>\n\nexport interface ExecuteOptions {\n readonly concurrency?: number | \"unbounded\"\n}\n\nexport const executeAllWithResolver = (\n tools: ReadonlyArray<AnyKindTool>,\n calls: ReadonlyArray<FunctionCall>,\n resolve: Resolver,\n options?: ExecuteOptions,\n): Stream.Stream<ToolEvent> =>\n Stream.fromIterable(calls).pipe(\n Stream.flatMap(\n (call) =>\n Stream.unwrap(\n resolve(call).pipe(Effect.map((decision) => dispatch(tools, call, decision))),\n ),\n { concurrency: options?.concurrency ?? \"unbounded\" },\n ),\n )\n\n/** No-resolver shortcut: every call gets `Execute`. */\nexport const executeAll = (\n tools: ReadonlyArray<AnyKindTool>,\n calls: ReadonlyArray<FunctionCall>,\n options?: ExecuteOptions,\n): Stream.Stream<ToolEvent> =>\n executeAllWithResolver(tools, calls, () => Effect.succeed(executeDecision), options)\n\nconst dispatch = (\n tools: ReadonlyArray<AnyKindTool>,\n call: FunctionCall,\n decision: ToolDecision,\n): Stream.Stream<ToolEvent> =>\n Match.value(decision).pipe(\n Match.tag(\"Execute\", () => runOne(tools, call)),\n Match.tag(\"Reject\", (d) =>\n Stream.succeed<ToolEvent>({ _tag: \"Output\", result: d.result }),\n ),\n Match.exhaustive,\n )\n\nconst valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResult => ({\n _tag: \"Value\",\n call_id: call.call_id,\n tool,\n value,\n})\n\nconst runOne = (\n tools: ReadonlyArray<AnyKindTool>,\n call: FunctionCall,\n): Stream.Stream<ToolEvent> => {\n const tool = tools.find((t) => t.name === call.name)\n if (tool === undefined) {\n // Graceful: emit a synthetic Failure so OTHER calls in this turn\n // still execute. LLMs hallucinate tool names; MCP tools come and go.\n return Stream.succeed<ToolEvent>({\n _tag: \"Output\",\n result: rejected(call, \"unknown_tool\", `No tool registered with name \"${call.name}\"`),\n })\n }\n if (isStreamingTool(tool)) return runStreaming(tool, call)\n return runPlain(tool, call)\n}\n\nconst runPlain = (\n tool: AnyPlainTool,\n call: FunctionCall,\n): Stream.Stream<ToolEvent> =>\n Stream.fromEffect(\n Effect.gen(function* () {\n const parsed = yield* Effect.try({\n try: () => JSON.parse(call.arguments) as unknown,\n catch: () => \"json_parse_error\" as const,\n })\n const validated = yield* Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: () => \"validation_threw\" as const,\n })\n if (validated.issues !== undefined) {\n return executionError(call, \"Tool input failed schema validation\")\n }\n const output = yield* tool.run(validated.value)\n return valueResult(call, tool.name, output)\n }).pipe(\n Effect.catchCause(() => Effect.succeed(executionError(call, \"Tool execution failed\"))),\n Effect.map((result) => ({ _tag: \"Output\", result }) satisfies ToolEvent),\n ),\n )\n\nconst runStreaming = (\n tool: AnyStreamingTool,\n call: FunctionCall,\n): Stream.Stream<ToolEvent> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const parsed = yield* Effect.try({\n try: () => JSON.parse(call.arguments) as unknown,\n catch: () => \"json_parse_error\" as const,\n })\n const validated = yield* Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: () => \"validation_threw\" as const,\n })\n if (validated.issues !== undefined) {\n return Stream.succeed<ToolEvent>({\n _tag: \"Output\",\n result: executionError(call, \"Tool input failed schema validation\"),\n })\n }\n\n // Real-time: tap each event into a Ref as it flows; emit one\n // Intermediate per event; then concat one synthetic Output element\n // built from the accumulated Ref via `finalize`.\n const ref = yield* Ref.make<Array<unknown>>([])\n const intermediates = tool.run(validated.value).pipe(\n Stream.tap((event) => Ref.update(ref, Arr.append(event))),\n Stream.map(\n (data) =>\n ({\n _tag: \"Intermediate\",\n call_id: call.call_id,\n tool: tool.name,\n data,\n }) satisfies ToolEvent,\n ),\n )\n const output = Stream.fromEffect(\n Ref.get(ref).pipe(\n Effect.map(\n (events) =>\n ({\n _tag: \"Output\",\n result: valueResult(call, tool.name, tool.finalize(events)),\n }) satisfies ToolEvent,\n ),\n ),\n )\n return intermediates.pipe(Stream.concat(output))\n }),\n ).pipe(\n Stream.catchCause(() =>\n Stream.succeed<ToolEvent>({\n _tag: \"Output\",\n result: executionError(call, \"Tool execution failed\"),\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// `nextStateFrom` - bridge from a `Stream<ToolEvent>` to the loop's emit\n// shape. Drains the stream to the consumer in real-time, taps every\n// `Output` into an internal Ref, and at end-of-stream emits\n// `Loop.next(build(results))`. Recipe never sees the Ref.\n// ---------------------------------------------------------------------------\n\nexport const nextStateFrom = <S>(\n stream: Stream.Stream<ToolEvent>,\n build: (results: ReadonlyArray<ToolResult>) => S,\n): Stream.Stream<Loop.Event<ToolEvent, S>> =>\n Loop.nextAfterFold(\n stream,\n [] as ReadonlyArray<ToolResult>,\n (acc, e) => (isOutput(e) ? Arr.append(acc, e.result) : acc),\n build,\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"Toolkit.mjs","names":["Arr","Loop.nextAfterFold"],"sources":["../../src/tool/Toolkit.ts"],"sourcesContent":["import { Array as Arr, Effect, Ref, Stream } from \"effect\"\nimport * as Loop from \"../loop/Loop.js\"\nimport type { FunctionCall } from \"../domain/Items.js\"\nimport {\n type AnyKindTool,\n type AnyPlainTool,\n type AnyStreamingTool,\n isStreamingTool,\n type Tool,\n type ToolDescriptor,\n} from \"./Tool.js\"\nimport {\n type ToolResult,\n executionError,\n rejected,\n} from \"./Outcome.js\"\nimport type { ToolEvent } from \"./ToolEvent.js\"\nimport { isOutput } from \"./ToolEvent.js\"\n\nexport type AnyTool = Tool<string, any, any, any>\n\nexport type Toolkit<Tools extends ReadonlyArray<AnyTool>> = {\n readonly tools: Tools\n}\n\nexport type ToolsR<Tools extends ReadonlyArray<AnyTool>> =\n Tools[number] extends Tool<any, any, any, infer R> ? R : never\n\nexport const make = <const Tools extends ReadonlyArray<AnyTool>>(tools: Tools): Toolkit<Tools> => ({\n tools,\n})\n\n/**\n * Render every tool in a toolkit to a provider-agnostic descriptor.\n * `inputSchema` is the JSON Schema document produced by the tool's\n * Standard Schema converter (draft 2020-12).\n */\nexport const toDescriptors = <Tools extends ReadonlyArray<AnyTool>>(\n toolkit: Toolkit<Tools>,\n): ReadonlyArray<ToolDescriptor> =>\n toolkit.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\n// ---------------------------------------------------------------------------\n// Tool executor. Streams `ToolEvent`s in real time and dispatches streaming\n// and plain tools uniformly. Policy stays outside this module: callers pass\n// only the calls they have already decided should run.\n// ---------------------------------------------------------------------------\n\nexport interface ExecuteOptions {\n readonly concurrency?: number | \"unbounded\"\n}\n\n/** Execute every provided call. Approval/rejection policy belongs upstream. */\nexport const executeAll = (\n tools: ReadonlyArray<AnyKindTool>,\n calls: ReadonlyArray<FunctionCall>,\n options?: ExecuteOptions,\n): Stream.Stream<ToolEvent> =>\n Stream.fromIterable(calls).pipe(\n Stream.flatMap((call) => runOne(tools, call), {\n concurrency: options?.concurrency ?? \"unbounded\",\n }),\n )\n\nexport const outputEvent = (result: ToolResult): ToolEvent => ({ _tag: \"Output\", result })\n\nexport const outputEvents = (\n results: ReadonlyArray<ToolResult>,\n): Stream.Stream<ToolEvent> => Stream.fromIterable(results.map(outputEvent))\n\nconst valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResult => ({\n _tag: \"Value\",\n call_id: call.call_id,\n tool,\n value,\n})\n\nconst runOne = (\n tools: ReadonlyArray<AnyKindTool>,\n call: FunctionCall,\n): Stream.Stream<ToolEvent> => {\n const tool = tools.find((t) => t.name === call.name)\n if (tool === undefined) {\n // Graceful: emit a synthetic Failure so OTHER calls in this turn\n // still execute. LLMs hallucinate tool names; MCP tools come and go.\n return Stream.succeed<ToolEvent>({\n _tag: \"Output\",\n result: rejected(call, \"unknown_tool\", `No tool registered with name \"${call.name}\"`),\n })\n }\n if (isStreamingTool(tool)) return runStreaming(tool, call)\n return runPlain(tool, call)\n}\n\nconst runPlain = (\n tool: AnyPlainTool,\n call: FunctionCall,\n): Stream.Stream<ToolEvent> =>\n Stream.fromEffect(\n Effect.gen(function* () {\n const parsed = yield* Effect.try({\n try: () => JSON.parse(call.arguments) as unknown,\n catch: () => \"json_parse_error\" as const,\n })\n const validated = yield* Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: () => \"validation_threw\" as const,\n })\n if (validated.issues !== undefined) {\n return executionError(call, \"Tool input failed schema validation\")\n }\n const output = yield* tool.run(validated.value)\n return valueResult(call, tool.name, output)\n }).pipe(\n Effect.catchCause(() => Effect.succeed(executionError(call, \"Tool execution failed\"))),\n Effect.map((result) => ({ _tag: \"Output\", result }) satisfies ToolEvent),\n ),\n )\n\nconst runStreaming = (\n tool: AnyStreamingTool,\n call: FunctionCall,\n): Stream.Stream<ToolEvent> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const parsed = yield* Effect.try({\n try: () => JSON.parse(call.arguments) as unknown,\n catch: () => \"json_parse_error\" as const,\n })\n const validated = yield* Effect.tryPromise({\n try: () => Promise.resolve(tool.inputSchema[\"~standard\"].validate(parsed)),\n catch: () => \"validation_threw\" as const,\n })\n if (validated.issues !== undefined) {\n return Stream.succeed<ToolEvent>({\n _tag: \"Output\",\n result: executionError(call, \"Tool input failed schema validation\"),\n })\n }\n\n // Real-time: tap each event into a Ref as it flows; emit one\n // Intermediate per event; then concat one synthetic Output element\n // built from the accumulated Ref via `finalize`.\n const ref = yield* Ref.make<Array<unknown>>([])\n const intermediates = tool.run(validated.value).pipe(\n Stream.tap((event) => Ref.update(ref, Arr.append(event))),\n Stream.map(\n (data) =>\n ({\n _tag: \"Intermediate\",\n call_id: call.call_id,\n tool: tool.name,\n data,\n }) satisfies ToolEvent,\n ),\n )\n const output = Stream.fromEffect(\n Ref.get(ref).pipe(\n Effect.map(\n (events) =>\n ({\n _tag: \"Output\",\n result: valueResult(call, tool.name, tool.finalize(events)),\n }) satisfies ToolEvent,\n ),\n ),\n )\n return intermediates.pipe(Stream.concat(output))\n }),\n ).pipe(\n Stream.catchCause(() =>\n Stream.succeed<ToolEvent>({\n _tag: \"Output\",\n result: executionError(call, \"Tool execution failed\"),\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// `nextStateFrom` - bridge from a `Stream<ToolEvent>` to the loop's emit\n// shape. Drains the stream to the consumer in real-time, taps every\n// `Output` into an internal Ref, and at end-of-stream emits\n// `Loop.next(build(results))`. Recipe never sees the Ref.\n// ---------------------------------------------------------------------------\n\nexport const nextStateFrom = <S>(\n stream: Stream.Stream<ToolEvent>,\n build: (results: ReadonlyArray<ToolResult>) => S,\n): Stream.Stream<Loop.Event<ToolEvent, S>> =>\n Loop.nextAfterFold(\n stream,\n [] as ReadonlyArray<ToolResult>,\n (acc, e) => (isOutput(e) ? Arr.append(acc, e.result) : acc),\n build,\n )\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Low-level primitives (loop, conversation, items, tools, streaming codecs) for building AI agents with Effect.",
5
5
  "keywords": [
6
6
  "agents",
@@ -86,27 +86,17 @@ export const assistantMessages = (turn: Turn): ReadonlyArray<Message> =>
86
86
  turn.items.filter((i): i is Message => i.type === "message" && i.role === "assistant")
87
87
 
88
88
  /**
89
- * State stamped with the just-completed `Turn`. Recipes use this as the
90
- * intermediate value between "turn lands" and "compute next state": extend
91
- * `state.history` with the turn's items, and keep the assembled turn
92
- * around for stop-reason / usage / function-call inspection.
93
- *
94
- * Generic over the recipe's state shape - any record carrying a
95
- * `history: ReadonlyArray<Item>` field works.
96
- */
97
- export type Cursor<S> = S & { readonly turn: Turn }
98
-
99
- /**
100
- * Build a `Cursor<S>` from a state record and the just-completed turn.
101
- * Extends `state.history` with `turn.items` and stamps the turn.
89
+ * Append a completed turn and optional follow-up items to a state record's
90
+ * history. Recipes use this at the point where structured tool results are
91
+ * converted to model-facing `FunctionCallOutput`s.
102
92
  */
103
- export const cursor = <S extends { readonly history: ReadonlyArray<Item> }>(
93
+ export const appendTurn = <S extends { readonly history: ReadonlyArray<Item> }>(
104
94
  state: S,
105
95
  turn: Turn,
106
- ): Cursor<S> => ({
96
+ items: ReadonlyArray<Item> = [],
97
+ ): S => ({
107
98
  ...state,
108
- history: [...state.history, ...turn.items],
109
- turn,
99
+ history: [...state.history, ...turn.items, ...items],
110
100
  })
111
101
 
112
102
  // ---------------------------------------------------------------------------
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Pre-execution decision (`ToolDecision`) and post-execution result
3
- * (`ToolResult`) for the resolver-based executor.
2
+ * Post-execution and synthetic tool results.
4
3
  *
5
- * - Resolver returns ToolDecision (Execute | Reject(result)) per call.
6
- * - Executor emits ToolResult (Value | Failure) per call.
4
+ * - Executed tools emit ToolResult.Value.
5
+ * - Approval/cancellation policy emits synthetic ToolResult.Failure.
7
6
  *
8
7
  * Wire conversion stays at the recipe boundary via `toFunctionCallOutput`
9
8
  * so recipes can inspect, redact, or audit values before serialization.
@@ -41,19 +40,6 @@ export const isValue = (r: ToolResult): r is Extract<ToolResult, { _tag: "Value"
41
40
  export const isFailure = (r: ToolResult): r is Extract<ToolResult, { _tag: "Failure" }> =>
42
41
  r._tag === "Failure"
43
42
 
44
- // ---------------------------------------------------------------------------
45
- // ToolDecision
46
- // ---------------------------------------------------------------------------
47
-
48
- export type ToolDecision =
49
- | { readonly _tag: "Execute" }
50
- | { readonly _tag: "Reject"; readonly result: ToolResult }
51
-
52
- export const execute: ToolDecision = { _tag: "Execute" }
53
-
54
- export const reject = (result: ToolResult): ToolDecision => ({ _tag: "Reject", result })
55
-
56
- // ---------------------------------------------------------------------------
57
43
  // Synthesizers. `denied` and `cancelled` are operationally distinct;
58
44
  // anything else is just a recipe-chosen `kind` via `rejected`.
59
45
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Tests for the resolver-based executor + resolvers + history-reconciliation
3
- * primitives. Exercises the full HITL + streaming-tool stack end-to-end via
4
- * `executeAllWithResolver`, with the four wire-shaped scenarios:
2
+ * Tests for approval planners + history-reconciliation primitives. Exercises
3
+ * the full HITL + streaming-tool stack end-to-end by composing approval plans
4
+ * with `executeAll`, with the four wire-shaped scenarios:
5
5
  *
6
6
  * 1. Approval : gated calls approved → tools execute, structured Values
7
7
  * 2. Denial : gated calls denied → Failure(denied) results
@@ -22,13 +22,12 @@ import {
22
22
  } from "./Outcome.js"
23
23
  import {
24
24
  type ApprovalMapEntry,
25
+ type ToolCallDecision,
25
26
  fromApprovalMap,
26
27
  fromVerdictQueue,
27
- withFallback,
28
- withPermissions,
29
28
  } from "./Resolvers.js"
30
29
  import { fromEffectSchema, make as makeTool, streaming } from "./Tool.js"
31
- import { executeAll, executeAllWithResolver } from "./Toolkit.js"
30
+ import { executeAll, outputEvent, outputEvents } from "./Toolkit.js"
32
31
  import {
33
32
  type ToolEvent,
34
33
  isApprovalRequested,
@@ -104,20 +103,28 @@ const resultsFrom = (
104
103
  const byCallId = (results: ReadonlyArray<ToolResult>) =>
105
104
  new Map(results.map((r) => [r.call_id, r]))
106
105
 
106
+ const eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {
107
+ const plan = fromApprovalMap(isSensitive, approvals)(calls)
108
+ return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected))
109
+ }
110
+
111
+ const eventsFromDecision = (decision: ToolCallDecision): Stream.Stream<ToolEvent> =>
112
+ decision._tag === "Approved"
113
+ ? executeAll(allTools, [decision.call])
114
+ : Stream.succeed(outputEvent(decision.result))
115
+
107
116
  // ---------------------------------------------------------------------------
108
117
  // fromApprovalMap: HTTP-style scenarios
109
118
  // ---------------------------------------------------------------------------
110
119
 
111
- describe("executeAllWithResolver + fromApprovalMap", () => {
120
+ describe("fromApprovalMap + executeAll", () => {
112
121
  it("approval: all gated approved → tools execute, structured Values", async () => {
113
122
  const approvals = new Map<string, ApprovalMapEntry>([
114
123
  ["c2", { decision: "approve" }],
115
124
  ["c3", { decision: "approve" }],
116
125
  ])
117
126
  const collected = await Effect.runPromise(
118
- Stream.runCollect(
119
- executeAllWithResolver(allTools, calls, fromApprovalMap(isSensitive, approvals)),
120
- ),
127
+ Stream.runCollect(eventsFromApprovalMap(approvals)),
121
128
  )
122
129
  const by = byCallId(resultsFrom(collected))
123
130
  expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
@@ -140,9 +147,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
140
147
  ["c3", { decision: "deny", reason: "prod is sacred" }],
141
148
  ])
142
149
  const collected = await Effect.runPromise(
143
- Stream.runCollect(
144
- executeAllWithResolver(allTools, calls, fromApprovalMap(isSensitive, approvals)),
145
- ),
150
+ Stream.runCollect(eventsFromApprovalMap(approvals)),
146
151
  )
147
152
 
148
153
  // bulk_email never ran.
@@ -165,13 +170,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
165
170
 
166
171
  it("cancellation: missing verdicts → Failure(cancelled)", async () => {
167
172
  const collected = await Effect.runPromise(
168
- Stream.runCollect(
169
- executeAllWithResolver(
170
- allTools,
171
- calls,
172
- fromApprovalMap(isSensitive, new Map()),
173
- ),
174
- ),
173
+ Stream.runCollect(eventsFromApprovalMap(new Map())),
175
174
  )
176
175
  const by = byCallId(resultsFrom(collected))
177
176
  expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
@@ -185,9 +184,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
185
184
  // c3 omitted → cancelled
186
185
  ])
187
186
  const collected = await Effect.runPromise(
188
- Stream.runCollect(
189
- executeAllWithResolver(allTools, calls, fromApprovalMap(isSensitive, approvals)),
190
- ),
187
+ Stream.runCollect(eventsFromApprovalMap(approvals)),
191
188
  )
192
189
  const by = byCallId(resultsFrom(collected))
193
190
  expect(by.get("c1")).toMatchObject({ _tag: "Value", value: { count: 3 } })
@@ -200,7 +197,7 @@ describe("executeAllWithResolver + fromApprovalMap", () => {
200
197
  // Graceful degradation: hallucinated tool name doesn't kill the turn.
201
198
  // ---------------------------------------------------------------------------
202
199
 
203
- describe("executeAllWithResolver: graceful degradation", () => {
200
+ describe("executeAll: graceful degradation", () => {
204
201
  it("unknown tool name → Failure(unknown_tool); other calls still execute", async () => {
205
202
  const callsWithBogus = [
206
203
  fc("c1", "web_search", { query: "x" }),
@@ -209,11 +206,13 @@ describe("executeAllWithResolver: graceful degradation", () => {
209
206
  ]
210
207
  const collected = await Effect.runPromise(
211
208
  Stream.runCollect(
212
- executeAllWithResolver(
213
- allTools,
214
- callsWithBogus,
215
- fromApprovalMap(isSensitive, new Map([["c3", { decision: "approve" }]])),
216
- ),
209
+ (() => {
210
+ const plan = fromApprovalMap(
211
+ isSensitive,
212
+ new Map([["c3", { decision: "approve" }]]),
213
+ )(callsWithBogus)
214
+ return Stream.merge(executeAll(allTools, plan.approved), outputEvents(plan.rejected))
215
+ })(),
217
216
  ),
218
217
  )
219
218
  const by = byCallId(resultsFrom(collected))
@@ -227,7 +226,7 @@ describe("executeAllWithResolver: graceful degradation", () => {
227
226
  // fromVerdictQueue: WebSocket-style scenarios
228
227
  // ---------------------------------------------------------------------------
229
228
 
230
- describe("executeAllWithResolver + fromVerdictQueue", () => {
229
+ describe("fromVerdictQueue + executeAll", () => {
231
230
  it("queue-driven: approve + deny resolve correctly with ApprovalRequested events", async () => {
232
231
  const collected = await Effect.runPromise(
233
232
  Effect.gen(function* () {
@@ -246,11 +245,17 @@ describe("executeAllWithResolver + fromVerdictQueue", () => {
246
245
  // Stream.unwrap supplies the Scope for fromVerdictQueue's router.
247
246
  const events = Stream.unwrap(
248
247
  Effect.gen(function* () {
249
- const { resolve, announce } = yield* fromVerdictQueue(
248
+ const { approved, decisions, announce } = yield* fromVerdictQueue(
250
249
  isSensitive,
251
250
  verdicts,
252
251
  )(calls)
253
- return Stream.merge(announce, executeAllWithResolver(allTools, calls, resolve))
252
+ return Stream.merge(
253
+ announce,
254
+ Stream.merge(
255
+ executeAll(allTools, approved),
256
+ decisions.pipe(Stream.flatMap(eventsFromDecision)),
257
+ ),
258
+ )
254
259
  }),
255
260
  )
256
261
  return yield* Stream.runCollect(events)
@@ -269,58 +274,6 @@ describe("executeAllWithResolver + fromVerdictQueue", () => {
269
274
  })
270
275
  })
271
276
 
272
- // ---------------------------------------------------------------------------
273
- // Combinators
274
- // ---------------------------------------------------------------------------
275
-
276
- describe("withPermissions / withFallback", () => {
277
- it("withPermissions short-circuits with permission_denied when canApprove returns false", async () => {
278
- const canApprove = (call: Items.FunctionCall) =>
279
- Effect.succeed(call.name !== "delete_database")
280
- const inner = fromApprovalMap(
281
- isSensitive,
282
- new Map<string, ApprovalMapEntry>([
283
- ["c2", { decision: "approve" }],
284
- ["c3", { decision: "approve" }],
285
- ]),
286
- )
287
- const collected = await Effect.runPromise(
288
- Stream.runCollect(
289
- executeAllWithResolver(allTools, calls, withPermissions(inner, canApprove)),
290
- ),
291
- )
292
- const by = byCallId(resultsFrom(collected))
293
- // c2 allowed → executed; c3 forbidden → permission_denied (no exec)
294
- expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
295
- expect(by.get("c3")).toMatchObject({
296
- _tag: "Failure",
297
- kind: "permission_denied",
298
- })
299
- })
300
-
301
- it("withFallback recovers a Reject by running an alternate decision", async () => {
302
- const inner = fromApprovalMap(
303
- isSensitive,
304
- new Map<string, ApprovalMapEntry>([
305
- ["c2", { decision: "deny", reason: "no" }],
306
- ["c3", { decision: "approve" }],
307
- ]),
308
- )
309
- // Recover only `denied` rejections; turn them into Execute (re-run anyway).
310
- const recoverable = (r: ToolResult) => isFailure(r) && r.kind === "denied"
311
- const fallbackResolver = withFallback(inner, recoverable, () =>
312
- Effect.succeed({ _tag: "Execute" } as const),
313
- )
314
- const collected = await Effect.runPromise(
315
- Stream.runCollect(executeAllWithResolver(allTools, calls, fallbackResolver)),
316
- )
317
- const by = byCallId(resultsFrom(collected))
318
- // c2 was denied but fallback re-ran the tool.
319
- expect(by.get("c2")).toMatchObject({ _tag: "Value", value: { status: "sent" } })
320
- expect(by.get("c3")).toMatchObject({ _tag: "Value", value: { status: "dropped" } })
321
- })
322
- })
323
-
324
277
  // ---------------------------------------------------------------------------
325
278
  // History reconciliation
326
279
  // ---------------------------------------------------------------------------
@@ -412,11 +365,11 @@ describe("toFunctionCallOutput", () => {
412
365
  })
413
366
 
414
367
  // ---------------------------------------------------------------------------
415
- // executeAll (no-resolver shortcut)
368
+ // executeAll
416
369
  // ---------------------------------------------------------------------------
417
370
 
418
371
  describe("executeAll", () => {
419
- it("equivalent to executeAllWithResolver with allow-all resolver", async () => {
372
+ it("runs all calls passed to it", async () => {
420
373
  const collected = await Effect.runPromise(
421
374
  Stream.runCollect(executeAll(allTools, calls)),
422
375
  )