effect-assistant 0.3.0 → 0.5.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 (91) hide show
  1. package/dist/Assistant.d.ts +5 -11
  2. package/dist/Assistant.d.ts.map +1 -1
  3. package/dist/Assistant.js.map +1 -1
  4. package/dist/AssistantEvent.d.ts +66 -7
  5. package/dist/AssistantEvent.d.ts.map +1 -1
  6. package/dist/AssistantEvent.js +74 -9
  7. package/dist/AssistantEvent.js.map +1 -1
  8. package/dist/AssistantLoop.d.ts +4 -2
  9. package/dist/AssistantLoop.d.ts.map +1 -1
  10. package/dist/AssistantLoop.js +4 -2
  11. package/dist/AssistantLoop.js.map +1 -1
  12. package/dist/Block.d.ts +63 -1
  13. package/dist/Block.d.ts.map +1 -1
  14. package/dist/Block.js +63 -2
  15. package/dist/Block.js.map +1 -1
  16. package/dist/BlockKit.d.ts +25 -3
  17. package/dist/BlockKit.d.ts.map +1 -1
  18. package/dist/BlockKit.js +14 -12
  19. package/dist/BlockKit.js.map +1 -1
  20. package/dist/Compaction.d.ts +1 -1
  21. package/dist/Compaction.d.ts.map +1 -1
  22. package/dist/Compaction.js +11 -4
  23. package/dist/Compaction.js.map +1 -1
  24. package/dist/Skill.d.ts +85 -43
  25. package/dist/Skill.d.ts.map +1 -1
  26. package/dist/Skill.js +93 -44
  27. package/dist/Skill.js.map +1 -1
  28. package/dist/Specialist.d.ts +6 -9
  29. package/dist/Specialist.d.ts.map +1 -1
  30. package/dist/Specialist.js.map +1 -1
  31. package/dist/Timeline.d.ts +44 -6
  32. package/dist/Timeline.d.ts.map +1 -1
  33. package/dist/Timeline.js +87 -16
  34. package/dist/Timeline.js.map +1 -1
  35. package/dist/ToolApproval.d.ts +146 -0
  36. package/dist/ToolApproval.d.ts.map +1 -0
  37. package/dist/ToolApproval.js +201 -0
  38. package/dist/ToolApproval.js.map +1 -0
  39. package/dist/ToolDiscovery.d.ts +6 -4
  40. package/dist/ToolDiscovery.d.ts.map +1 -1
  41. package/dist/ToolDiscovery.js +5 -4
  42. package/dist/ToolDiscovery.js.map +1 -1
  43. package/dist/ToolResult.d.ts +56 -0
  44. package/dist/ToolResult.d.ts.map +1 -0
  45. package/dist/ToolResult.js +49 -0
  46. package/dist/ToolResult.js.map +1 -0
  47. package/dist/index.d.ts +4 -2
  48. package/dist/index.js +6 -4
  49. package/dist/internal/chat.js +4 -1
  50. package/dist/internal/chat.js.map +1 -1
  51. package/dist/internal/history.js +28 -3
  52. package/dist/internal/history.js.map +1 -1
  53. package/dist/internal/loop.d.ts +10 -5
  54. package/dist/internal/loop.d.ts.map +1 -1
  55. package/dist/internal/loop.js +60 -21
  56. package/dist/internal/loop.js.map +1 -1
  57. package/dist/internal/specialists.js +9 -16
  58. package/dist/internal/specialists.js.map +1 -1
  59. package/dist/internal/toolkit.d.ts +11 -7
  60. package/dist/internal/toolkit.d.ts.map +1 -1
  61. package/dist/internal/toolkit.js +74 -21
  62. package/dist/internal/toolkit.js.map +1 -1
  63. package/dist/internal/tools.js +39 -6
  64. package/dist/internal/tools.js.map +1 -1
  65. package/dist/internal/turns.d.ts.map +1 -1
  66. package/dist/internal/turns.js +24 -16
  67. package/dist/internal/turns.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/Assistant.ts +7 -13
  70. package/src/AssistantEvent.ts +73 -8
  71. package/src/AssistantLoop.ts +4 -2
  72. package/src/Block.ts +82 -1
  73. package/src/BlockKit.ts +37 -4
  74. package/src/Compaction.ts +19 -2
  75. package/src/Skill.ts +134 -57
  76. package/src/Specialist.ts +8 -11
  77. package/src/Timeline.ts +120 -21
  78. package/src/ToolApproval.ts +249 -0
  79. package/src/ToolDiscovery.ts +8 -3
  80. package/src/ToolResult.ts +80 -0
  81. package/src/index.ts +10 -0
  82. package/src/internal/chat.ts +11 -1
  83. package/src/internal/history.ts +58 -3
  84. package/src/internal/loop.ts +138 -35
  85. package/src/internal/specialists.ts +7 -13
  86. package/src/internal/toolkit.ts +102 -35
  87. package/src/internal/tools.ts +81 -5
  88. package/src/internal/turns.ts +31 -10
  89. package/dist/internal/skills.js +0 -33
  90. package/dist/internal/skills.js.map +0 -1
  91. package/src/internal/skills.ts +0 -48
@@ -1,4 +1,5 @@
1
1
  import type * as LanguageModel from "effect/ai/LanguageModel"
2
+ import * as Response from "effect/ai/Response"
2
3
  import type * as Tool from "effect/ai/Tool"
3
4
  import * as Cause from "effect/Cause"
4
5
  import * as Context from "effect/Context"
@@ -16,11 +17,10 @@ import type { ChatId } from "../ChatId.ts"
16
17
  import * as AssistantEvent from "../AssistantEvent.ts"
17
18
  import type * as AssistantLoop from "../AssistantLoop.ts"
18
19
  import * as Loop from "./loop.ts"
19
- import type * as BlockKit from "../BlockKit.ts"
20
+ import * as BlockKit from "../BlockKit.ts"
20
21
  import type { CurrentChat } from "../CurrentChat.ts"
21
22
  import * as Instructions from "../Instructions.ts"
22
- import * as Skill from "../Skill.ts"
23
- import * as Skills from "./skills.ts"
23
+ import * as ToolApproval from "../ToolApproval.ts"
24
24
  import type * as Specialist from "../Specialist.ts"
25
25
  import * as AppEvents from "./events.ts"
26
26
  import * as History from "./history.ts"
@@ -125,6 +125,21 @@ const failureMessage = (cause: Cause.Cause<unknown>): string =>
125
125
  onSome: ({ _tag, raw }) => `${_tag ?? "Error"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`
126
126
  })
127
127
 
128
+ // Approved, and not run yet: also one a crash cut off while it ran, which runs again with the same id.
129
+ const approvedCalls = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlyArray<AssistantEvent.ToolCallPart> =>
130
+ [...ToolApproval.fromEntries(entries).values()].flatMap(({ request, status, outcome }) =>
131
+ status === "approved" && outcome === undefined
132
+ ? [
133
+ Response.makePart("tool-call", {
134
+ id: request.callId,
135
+ name: request.tool,
136
+ params: request.params,
137
+ providerExecuted: false
138
+ })
139
+ ]
140
+ : []
141
+ )
142
+
128
143
  const lastSeq = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) => entries.at(-1)?.seq ?? otherwise
129
144
 
130
145
  // The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a
@@ -199,6 +214,7 @@ const makeTurnApi = (
199
214
  ): Assistant.Turn<any> => {
200
215
  const { definition, builtins } = registration
201
216
  const { app } = definition
217
+ const shownTools = BlockKit.toolNames(definition.blocks)
202
218
 
203
219
  const running = <A, E, R>(stream: Stream.Stream<A, E, R>) =>
204
220
  Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)))
@@ -265,8 +281,12 @@ const makeTurnApi = (
265
281
  }
266
282
  }),
267
283
  // A second step, for a reply after a call it was refused.
268
- maxSteps: 2
269
- })
284
+ maxSteps: 2,
285
+ // The pass runs because the model wrote text of its own instead of replying, which reaches nobody: it has to
286
+ // call a tool now. Changing the choice costs a read of the history at full price, on these rare turns only.
287
+ toolChoice: "required"
288
+ // The loop ran the approved calls; a pass that can only reply runs none.
289
+ }).pipe(Stream.provideService(Loop.TurnApprovals, Option.none()))
270
290
  })
271
291
  )
272
292
 
@@ -280,13 +300,12 @@ const makeTurnApi = (
280
300
  })
281
301
  )
282
302
 
283
- const prepare = <T extends Record<string, Tool.Any>, RS>(options: Assistant.LoopOptions<T, RS, any>) => {
284
- const own = Toolkits.fromToolkitOrEmpty(options.toolkit)
285
- const skills = Skills.available(options.skills ?? [], [...builtins.definitions, ...own.definitions])
303
+ const prepare = <T extends Record<string, Tool.Any>>(options: Assistant.LoopOptions<T, any>) => {
304
+ const own = Toolkits.showing(Toolkits.fromToolkitOrEmpty(options.toolkit), shownTools)
286
305
 
287
306
  return {
288
- system: mapText(instructions(options.instructions), (text) => Skill.withCatalog(text, skills)),
289
- toolkit: Toolkits.merge(Toolkits.merge(builtins, Skills.toTools(skills)), own),
307
+ system: instructions(options.instructions),
308
+ toolkit: Toolkits.merge(builtins, own),
290
309
  view: options.history ?? identity
291
310
  }
292
311
  }
@@ -357,6 +376,8 @@ export const run = Effect.fnUntraced(
357
376
  Loop.TurnTools,
358
377
  Option.some(Toolkits.provide(options.registration.builtins, options.services))
359
378
  ),
379
+ // A turn's loop waits for the user's approval, and runs the calls they approved.
380
+ Effect.provideService(Loop.TurnApprovals, Option.some({ approved: Effect.map(chat.entries, approvedCalls) })),
360
381
  Effect.provideContext(options.services),
361
382
  Effect.matchCauseEffect({
362
383
  onSuccess: Effect.fnUntraced(function* () {
@@ -1,33 +0,0 @@
1
- import { ToolFailure, fail } from "../ToolFailure.js";
2
- import { empty, withContext } from "./toolkit.js";
3
- import { resolve } from "../Instructions.js";
4
- import * as Effect from "effect/Effect";
5
- import * as Schema from "effect/Schema";
6
- import * as Tool from "effect/ai/Tool";
7
- import * as Toolkit from "effect/ai/Toolkit";
8
- //#region src/internal/skills.ts
9
- /** @internal */
10
- const available = (skills, definitions) => {
11
- const names = new Set(definitions.map((tool) => tool.name));
12
- return skills.filter((skill) => (skill.tools ?? []).every((tool) => names.has(tool.name)));
13
- };
14
- /** @internal */
15
- const toTools = (skills) => {
16
- if (skills.length === 0) return empty;
17
- const byName = new Map(skills.map((skill) => [skill.name, skill]));
18
- const LoadSkill = Tool.make("load_skill", {
19
- description: "Load a skill's instructions by name, in the same step as your first lookups (not in a step of its own). Follow them for the rest of the task.",
20
- parameters: Schema.Struct({ name: Schema.Literals(skills.map((skill) => skill.name)) }),
21
- success: Schema.String,
22
- failure: ToolFailure
23
- });
24
- return withContext(Toolkit.make(LoadSkill), (context) => ({ load_skill: Effect.fnUntraced(function* ({ name }) {
25
- const skill = byName.get(name);
26
- if (skill === void 0) return yield* fail(`No skill named ${name}.`);
27
- return (yield* resolve(skill.instructions).pipe(Effect.provideContext(context))).text;
28
- }) }));
29
- };
30
- //#endregion
31
- export { available, toTools };
32
-
33
- //# sourceMappingURL=skills.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"skills.js","names":["Toolkits.empty","ToolFailure.ToolFailure","Toolkits.withContext","ToolFailure.fail","Instructions.resolve"],"sources":["../../src/internal/skills.ts"],"sourcesContent":["import * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport type * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport * as Instructions from \"../Instructions.ts\"\nimport type { Skill } from \"../Skill.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\n\n// A skill whose tools the loop doesn't have would send the model after calls it can't make.\n/** @internal */\nexport const available = <R>(\n skills: ReadonlyArray<Skill<R>>,\n definitions: ReadonlyArray<Tool.Any>\n): ReadonlyArray<Skill<R>> => {\n const names = new Set(definitions.map((tool) => tool.name))\n\n return skills.filter((skill) => (skill.tools ?? []).every((tool) => names.has(tool.name)))\n}\n\n/** @internal */\nexport const toTools = <R>(skills: ReadonlyArray<Skill<R>>): Toolkits.Tools<R> => {\n if (skills.length === 0) return Toolkits.empty\n\n const byName = new Map(skills.map((skill) => [skill.name, skill]))\n\n const LoadSkill = Tool.make(\"load_skill\", {\n description:\n \"Load a skill's instructions by name, in the same step as your first lookups (not in a step of its own). Follow them for the rest of the task.\",\n parameters: Schema.Struct({ name: Schema.Literals(skills.map((skill) => skill.name)) }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure\n })\n\n // A skill's instructions resolve with the services of the loop that loads it.\n return Toolkits.withContext(Toolkit.make(LoadSkill), (context: Context.Context<R>) => ({\n load_skill: Effect.fnUntraced(function* ({ name }) {\n const skill = byName.get(name)\n\n if (skill === undefined) return yield* ToolFailure.fail(`No skill named ${name}.`)\n\n const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))\n\n return resolved.text\n })\n }))\n}\n"],"mappings":";;;;;;;;;AAYA,MAAa,aACX,QACA,gBAC4B;CAC5B,MAAM,QAAQ,IAAI,IAAI,YAAY,KAAK,SAAS,KAAK,IAAI,CAAC;CAE1D,OAAO,OAAO,QAAQ,WAAW,MAAM,SAAS,CAAC,EAAA,CAAG,OAAO,SAAS,MAAM,IAAI,KAAK,IAAI,CAAC,CAAC;AAC3F;;AAGA,MAAa,WAAc,WAAuD;CAChF,IAAI,OAAO,WAAW,GAAG,OAAOA;CAEhC,MAAM,SAAS,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CAEjE,MAAM,YAAY,KAAK,KAAK,cAAc;EACxC,aACE;EACF,YAAY,OAAO,OAAO,EAAE,MAAM,OAAO,SAAS,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC,EAAE,CAAC;EACtF,SAAS,OAAO;EAChB,SAASC;CACX,CAAC;CAGD,OAAOC,YAAqB,QAAQ,KAAK,SAAS,IAAI,aAAiC,EACrF,YAAY,OAAO,WAAW,WAAW,EAAE,QAAQ;EACjD,MAAM,QAAQ,OAAO,IAAI,IAAI;EAE7B,IAAI,UAAU,KAAA,GAAW,OAAO,OAAOC,KAAiB,kBAAkB,KAAK,EAAE;EAIjF,QAAO,OAFiBC,QAAqB,MAAM,YAAY,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,CAAC,EAAA,CAEpF;CAClB,CAAC,EACH,EAAE;AACJ"}
@@ -1,48 +0,0 @@
1
- import * as Tool from "effect/ai/Tool"
2
- import * as Toolkit from "effect/ai/Toolkit"
3
- import type * as Context from "effect/Context"
4
- import * as Effect from "effect/Effect"
5
- import * as Schema from "effect/Schema"
6
- import * as Instructions from "../Instructions.ts"
7
- import type { Skill } from "../Skill.ts"
8
- import * as ToolFailure from "../ToolFailure.ts"
9
- import * as Toolkits from "./toolkit.ts"
10
-
11
- // A skill whose tools the loop doesn't have would send the model after calls it can't make.
12
- /** @internal */
13
- export const available = <R>(
14
- skills: ReadonlyArray<Skill<R>>,
15
- definitions: ReadonlyArray<Tool.Any>
16
- ): ReadonlyArray<Skill<R>> => {
17
- const names = new Set(definitions.map((tool) => tool.name))
18
-
19
- return skills.filter((skill) => (skill.tools ?? []).every((tool) => names.has(tool.name)))
20
- }
21
-
22
- /** @internal */
23
- export const toTools = <R>(skills: ReadonlyArray<Skill<R>>): Toolkits.Tools<R> => {
24
- if (skills.length === 0) return Toolkits.empty
25
-
26
- const byName = new Map(skills.map((skill) => [skill.name, skill]))
27
-
28
- const LoadSkill = Tool.make("load_skill", {
29
- description:
30
- "Load a skill's instructions by name, in the same step as your first lookups (not in a step of its own). Follow them for the rest of the task.",
31
- parameters: Schema.Struct({ name: Schema.Literals(skills.map((skill) => skill.name)) }),
32
- success: Schema.String,
33
- failure: ToolFailure.ToolFailure
34
- })
35
-
36
- // A skill's instructions resolve with the services of the loop that loads it.
37
- return Toolkits.withContext(Toolkit.make(LoadSkill), (context: Context.Context<R>) => ({
38
- load_skill: Effect.fnUntraced(function* ({ name }) {
39
- const skill = byName.get(name)
40
-
41
- if (skill === undefined) return yield* ToolFailure.fail(`No skill named ${name}.`)
42
-
43
- const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))
44
-
45
- return resolved.text
46
- })
47
- }))
48
- }