effect-assistant 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) 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 +55 -7
  5. package/dist/AssistantEvent.d.ts.map +1 -1
  6. package/dist/AssistantEvent.js +61 -7
  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/Compaction.d.ts +1 -1
  13. package/dist/Compaction.d.ts.map +1 -1
  14. package/dist/Compaction.js +13 -6
  15. package/dist/Compaction.js.map +1 -1
  16. package/dist/Skill.d.ts +85 -43
  17. package/dist/Skill.d.ts.map +1 -1
  18. package/dist/Skill.js +93 -44
  19. package/dist/Skill.js.map +1 -1
  20. package/dist/Specialist.d.ts +6 -9
  21. package/dist/Specialist.d.ts.map +1 -1
  22. package/dist/Specialist.js.map +1 -1
  23. package/dist/Timeline.d.ts +40 -6
  24. package/dist/Timeline.d.ts.map +1 -1
  25. package/dist/Timeline.js +74 -15
  26. package/dist/Timeline.js.map +1 -1
  27. package/dist/ToolApproval.d.ts +146 -0
  28. package/dist/ToolApproval.d.ts.map +1 -0
  29. package/dist/ToolApproval.js +201 -0
  30. package/dist/ToolApproval.js.map +1 -0
  31. package/dist/ToolDiscovery.d.ts +6 -4
  32. package/dist/ToolDiscovery.d.ts.map +1 -1
  33. package/dist/ToolDiscovery.js +5 -4
  34. package/dist/ToolDiscovery.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.js +3 -2
  37. package/dist/internal/chat.js +4 -1
  38. package/dist/internal/chat.js.map +1 -1
  39. package/dist/internal/history.js +28 -3
  40. package/dist/internal/history.js.map +1 -1
  41. package/dist/internal/loop.d.ts +10 -5
  42. package/dist/internal/loop.d.ts.map +1 -1
  43. package/dist/internal/loop.js +54 -21
  44. package/dist/internal/loop.js.map +1 -1
  45. package/dist/internal/specialists.js +9 -16
  46. package/dist/internal/specialists.js.map +1 -1
  47. package/dist/internal/toolkit.d.ts +5 -7
  48. package/dist/internal/toolkit.d.ts.map +1 -1
  49. package/dist/internal/toolkit.js +28 -18
  50. package/dist/internal/toolkit.js.map +1 -1
  51. package/dist/internal/turns.d.ts.map +1 -1
  52. package/dist/internal/turns.js +18 -13
  53. package/dist/internal/turns.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/Assistant.ts +7 -13
  56. package/src/AssistantEvent.ts +60 -6
  57. package/src/AssistantLoop.ts +4 -2
  58. package/src/Compaction.ts +19 -2
  59. package/src/Skill.ts +134 -57
  60. package/src/Specialist.ts +8 -11
  61. package/src/Timeline.ts +104 -18
  62. package/src/ToolApproval.ts +249 -0
  63. package/src/ToolDiscovery.ts +8 -3
  64. package/src/index.ts +5 -0
  65. package/src/internal/chat.ts +11 -1
  66. package/src/internal/history.ts +58 -3
  67. package/src/internal/loop.ts +120 -34
  68. package/src/internal/specialists.ts +7 -13
  69. package/src/internal/toolkit.ts +47 -30
  70. package/src/internal/turns.ts +24 -7
  71. package/dist/internal/skills.js +0 -33
  72. package/dist/internal/skills.js.map +0 -1
  73. package/src/internal/skills.ts +0 -48
@@ -1 +1 @@
1
- {"version":3,"file":"specialists.js","names":["run","Toolkits.fromToolkitOrEmpty","Skills.available","Loop.run","Skill.withCatalog","Toolkits.merge","Skills.toTools","Telemetry.agentOutput","Telemetry.agentSpan","InputId.fromKey","RunId.RunId","ToolFailure.ToolFailure","Specialist.Runner","ToolFailure.fail","Toolkits.empty","Toolkits.fromWithHandler","Loop.TurnTools","Specialist.SpecialistError","RunId.make"],"sources":["../../src/internal/specialists.ts"],"sourcesContent":["import type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport * as Prompt from \"effect/ai/Prompt\"\nimport * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Fiber from \"effect/Fiber\"\nimport * as FiberMap from \"effect/FiberMap\"\nimport * as FiberSet from \"effect/FiberSet\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Queue from \"effect/Queue\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport * as Stream from \"effect/Stream\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type * as AssistantLoop from \"../AssistantLoop.ts\"\nimport * as Loop from \"./loop.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as InputId from \"../InputId.ts\"\nimport * as RunId from \"../RunId.ts\"\nimport * as Skill from \"../Skill.ts\"\nimport * as Skills from \"./skills.ts\"\nimport * as Specialist from \"../Specialist.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport { toolMessage, userText } from \"./history.ts\"\nimport { mapText } from \"./instructions.ts\"\nimport * as Telemetry from \"./telemetry.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport type { Writer } from \"./writer.ts\"\n\n/** @internal */\nexport type RunHandler = (run: Specialist.Run) => Stream.Stream<AssistantLoop.Event, unknown, Specialist.RunServices>\n\n/** @internal */\nexport const briefText = (brief: Specialist.Brief): string =>\n [\n `Goal: ${brief.goal}`,\n brief.context && `Context: ${brief.context}`,\n brief.successCriteria && `Done when: ${brief.successCriteria}`,\n brief.report && `Report back: ${brief.report}`,\n brief.limits && `Limits: ${brief.limits}`\n ]\n .filter(Boolean)\n .join(\"\\n\")\n\nconst finalText = (output: AssistantEvent.ModelOutput): string =>\n output.parts\n .flatMap((part) => (part.type === \"text\" ? [part.text] : []))\n .join(\"\")\n .trim()\n\n// The answer is the last text the model wrote.\n/** @internal */\nexport const runBrief = Effect.fnUntraced(\n function* (\n handler: RunHandler,\n options: {\n readonly specialist: Specialist.Any\n readonly runId: RunId.RunId\n readonly brief: Specialist.Brief\n readonly history: Ref.Ref<ReadonlyArray<Prompt.Message>>\n readonly inbox: Queue.Queue<Prompt.Message>\n readonly chatId: Writer[\"chatId\"]\n }\n ) {\n const { history, inbox } = options\n const answer = yield* Ref.make(\"\")\n const add = (messages: ReadonlyArray<Prompt.Message>) => Ref.update(history, (all) => [...all, ...messages])\n\n // Read before every model call, when no tool calls are open: messages sent meanwhile join it now.\n const current = Effect.gen(function* () {\n yield* add(yield* Queue.clear(inbox))\n\n return yield* Ref.get(history)\n })\n\n const run: Specialist.Run = {\n runId: options.runId,\n chatId: options.chatId,\n brief: options.brief,\n history: current,\n loop: (loop) => {\n const own = Toolkits.fromToolkitOrEmpty(loop.toolkit)\n const skills = Skills.available(loop.skills ?? [], own.definitions)\n\n return Loop.run({\n instructions: mapText(loop.instructions, (text) => Skill.withCatalog(text, skills)),\n toolkit: Toolkits.merge(Skills.toTools(skills), own),\n history: current,\n maxSteps: loop.maxSteps ?? 8\n })\n }\n }\n\n // A run lives in memory, so there's nothing to run again after a crash: `ToolStarted` isn't kept.\n const record = Effect.fnUntraced(function* (event: AssistantLoop.Event) {\n if (Predicate.isTagged(event, \"ToolStarted\")) return\n\n if (Predicate.isTagged(event, \"ToolResult\")) return yield* add([toolMessage(event)])\n\n yield* add(Prompt.fromResponseParts(event.parts).content)\n const text = finalText(event)\n\n if (text.length > 0) yield* Ref.set(answer, text)\n })\n\n yield* Stream.runForEach(handler(run), record)\n\n const text = yield* Ref.get(answer)\n\n yield* Telemetry.agentOutput(text)\n\n return text\n },\n (effect, _handler, options) => effect.pipe(Telemetry.agentSpan(options.specialist.name, { chatId: options.chatId }))\n)\n\ntype Outcome =\n | { readonly status: \"answered\"; readonly report: string }\n | { readonly status: \"failed\"; readonly error: string }\n | { readonly status: \"stopped\" | \"cut_off\" }\n\n// Only `stop` interrupts a brief's loop alone; a failure or a defect failed it.\nconst unanswered = (cause: Cause.Cause<unknown>): Outcome =>\n Cause.hasInterruptsOnly(cause) ? { status: \"stopped\" } : { status: \"failed\", error: String(Cause.squash(cause)) }\n\n// The id is fixed per run and brief, so a replay is stored once.\n/** @internal */\nexport const finished = (\n specialist: string,\n run: { readonly runId: RunId.RunId; readonly brief: number; readonly goal: string },\n outcome: Outcome\n): AssistantEvent.SpecialistFinished =>\n AssistantEvent.SpecialistFinished.make({\n id: InputId.fromKey(`${run.runId}:${run.brief}`),\n runId: run.runId,\n specialist,\n brief: run.brief,\n goal: run.goal,\n ...outcome\n })\n\nconst RunIdInput = RunId.RunId.annotate({ description: \"The runId spawn_specialist returned.\" })\n\nconst RunStatus = <const Status extends string>(status: ReadonlyArray<Status>) =>\n Schema.Struct({ runId: RunId.RunId, status: Schema.Literals(status) })\n\nconst MessageSpecialist = Tool.make(\"message_specialist\", {\n description:\n \"Send more to one of your specialists: new instructions for a running one (it reads them at its next step), or a follow-up for a finished one (it continues with what it already found).\",\n parameters: Schema.Struct({ runId: RunIdInput, message: Schema.String }),\n success: RunStatus([\"steered\", \"continued\"]),\n failure: ToolFailure.ToolFailure,\n dependencies: [Specialist.Runner]\n})\n\nconst StopSpecialist = Tool.make(\"stop_specialist\", {\n description:\n \"Stop one of your running specialists, for example when the user changed their mind. It posts no answer.\",\n parameters: Schema.Struct({ runId: RunIdInput }),\n success: RunStatus([\"stopped\"]),\n failure: ToolFailure.ToolFailure,\n dependencies: [Specialist.Runner]\n})\n\nconst refused = <A, R>(effect: Effect.Effect<A, Specialist.SpecialistError, R>) =>\n Effect.catch(effect, (error) => ToolFailure.fail(error.message, { reason: error.reason }))\n\n/** @internal */\nexport const toolkit = Effect.fnUntraced(function* (\n specialists: ReadonlyArray<Specialist.Any>\n): Effect.fn.Return<Toolkits.Tools<Specialist.Runner>> {\n if (specialists.length === 0) return Toolkits.empty\n\n const catalog = specialists.map((specialist) => `- ${specialist.name}: ${specialist.description}`).join(\"\\n\")\n\n const SpawnSpecialist = Tool.make(\"spawn_specialist\", {\n description: `Start a specialist on a job. It works in the background and its report arrives as a <specialist> input, often within seconds. Write a self-contained brief: it can't see this chat. For a quick question, just call wait; for slow work, tell the user in one line first.\\nPass one of these as specialist:\\n${catalog}`,\n parameters: Schema.Struct({\n specialist: Schema.Literals(specialists.map((specialist) => specialist.name)).annotate({\n description: \"The specialist to start, by name.\"\n }),\n ...Specialist.Brief.fields\n }),\n success: RunStatus([\"started\"]),\n failure: ToolFailure.ToolFailure,\n dependencies: [Specialist.Runner]\n })\n\n const SpecialistTools = Toolkit.make(SpawnSpecialist, MessageSpecialist, StopSpecialist)\n\n const handlers = yield* SpecialistTools.toHandlers({\n spawn_specialist: Effect.fnUntraced(function* ({ specialist, ...brief }) {\n const runId = yield* refused(Specialist.Runner.use((runner) => runner.spawn({ specialist, brief })))\n\n return { runId, status: \"started\" }\n }),\n message_specialist: Effect.fnUntraced(function* ({ runId, message }) {\n const status = yield* refused(Specialist.Runner.use((runner) => runner.message(runId, message)))\n\n return { runId, status }\n }),\n stop_specialist: Effect.fnUntraced(function* ({ runId }) {\n yield* refused(Specialist.Runner.use((runner) => runner.stop(runId)))\n\n return { runId, status: \"stopped\" }\n })\n })\n\n return Toolkits.fromWithHandler(yield* Effect.provideContext(SpecialistTools, handlers))\n})\n\n// How long a finished run stays available for follow-ups.\nconst warm = \"15 minutes\"\n\ninterface Run {\n readonly runId: RunId.RunId\n readonly specialist: Specialist.Any\n readonly handler: RunHandler\n readonly goal: string\n readonly brief: Specialist.Brief\n readonly history: Ref.Ref<ReadonlyArray<Prompt.Message>>\n // Messages sent while it works; the loop reads them at its next step.\n readonly inbox: Queue.Queue<Prompt.Message>\n // Counts follow-ups too; numbers the reports.\n readonly briefs: Ref.Ref<number>\n // Read and changed under `lock`.\n readonly busy: Ref.Ref<boolean>\n // Orders a message against the run finishing, so none is dropped or starts a second loop.\n readonly lock: Semaphore.Semaphore\n}\n\n/** @internal */\nexport const makeRunner = Effect.fnUntraced(function* (options: {\n readonly chat: Writer\n readonly handle: CurrentChat[\"Service\"]\n readonly specialists: ReadonlyArray<{\n readonly specialist: Specialist.Any\n readonly handler: RunHandler\n }>\n readonly model: Context.Context<LanguageModel.LanguageModel>\n // An interruption keeps the hold.\n readonly holding: (effect: Effect.Effect<void>) => Effect.Effect<void>\n readonly report: (finished: AssistantEvent.SpecialistFinished) => Effect.Effect<void>\n}) {\n const { chat } = options\n const runs = new Map<string, Run>()\n const byName = new Map(options.specialists.map((entry) => [entry.specialist.name, entry]))\n // The brief loops working now, and the finished runs' countdowns to being forgotten.\n const working = yield* FiberMap.make<string, Outcome>()\n const forgetting = yield* FiberMap.make<string>()\n const background = yield* FiberSet.make()\n\n const report = Effect.fnUntraced(function* (run: Run, outcome: Outcome) {\n const brief = yield* Ref.get(run.briefs)\n\n yield* options.report(finished(run.specialist.name, { runId: run.runId, brief, goal: run.goal }, outcome))\n })\n\n const once = (run: Run) =>\n runBrief(run.handler, {\n specialist: run.specialist,\n runId: run.runId,\n brief: run.brief,\n history: run.history,\n inbox: run.inbox,\n chatId: chat.chatId\n }).pipe(\n Effect.provideContext(Context.merge(options.model, services)),\n // Spawned from a turn, a run would inherit the turn's own tools: a run can't message the user.\n Effect.provideService(Loop.TurnTools, Option.none()),\n Effect.matchCause({\n onSuccess: (answer): Outcome => ({ status: \"answered\", report: answer }),\n onFailure: unanswered\n })\n )\n\n // A message that arrived after the last read: keep working before reporting.\n const untilCaughtUp = Effect.fnUntraced(function* (run: Run) {\n let outcome = yield* once(run)\n\n while ((yield* Queue.size(run.inbox)) > 0) outcome = yield* once(run)\n\n return outcome\n })\n\n const work = Effect.fnUntraced(\n function* (run: Run) {\n yield* chat.append([\n AssistantEvent.SpecialistStarted.make({\n runId: run.runId,\n specialist: run.specialist.name,\n goal: run.goal\n })\n ])\n\n let finished = false\n\n while (!finished) {\n const exit = yield* Fiber.await(yield* FiberMap.run(working, run.runId, untilCaughtUp(run)))\n const outcome = Exit.isSuccess(exit) ? exit.value : unanswered(exit.cause)\n\n finished = yield* run.lock.withPermit(\n Effect.gen(function* () {\n // A message steered in after the loop's last read: work on it before reporting.\n if (outcome.status !== \"stopped\" && (yield* Queue.size(run.inbox)) > 0) return false\n\n yield* report(run, outcome)\n yield* Ref.set(run.busy, false)\n yield* FiberMap.run(\n forgetting,\n run.runId,\n Effect.gen(function* () {\n yield* Effect.sleep(warm)\n runs.delete(run.runId)\n })\n )\n\n return true\n })\n )\n }\n },\n // A shutdown interrupts the whole run: no turn can start now, so the report waits for the chat's next start.\n (effect, run) => Effect.onInterrupt(effect, () => report(run, { status: \"cut_off\" }))\n )\n\n const launch = Effect.fnUntraced(function* (run: Run) {\n yield* FiberMap.remove(forgetting, run.runId)\n yield* FiberSet.run(background, options.holding(work(run)))\n })\n\n // Under the run's lock.\n const send = Effect.fnUntraced(function* (run: Run, text: string): Effect.fn.Return<\"steered\" | \"continued\"> {\n yield* Ref.update(run.briefs, (n) => n + 1)\n\n if (yield* Ref.get(run.busy)) {\n yield* Queue.offer(run.inbox, userText(text))\n\n return \"steered\"\n }\n\n yield* Ref.update(run.history, (all) => [...all, userText(text)])\n yield* Ref.set(run.busy, true)\n yield* launch(run)\n\n return \"continued\"\n })\n\n const runner = Specialist.Runner.of({\n spawn: Effect.fnUntraced(function* (request: Specialist.SpawnRequest) {\n const found = byName.get(request.specialist)\n\n if (found === undefined) {\n return yield* new Specialist.SpecialistError({\n reason: \"UnknownSpecialist\",\n message: `No specialist named ${request.specialist}.`\n })\n }\n\n const run: Run = {\n runId: yield* RunId.make(),\n specialist: found.specialist,\n handler: found.handler,\n goal: request.brief.goal,\n brief: request.brief,\n history: yield* Ref.make<ReadonlyArray<Prompt.Message>>([userText(briefText(request.brief))]),\n inbox: yield* Queue.unbounded<Prompt.Message>(),\n briefs: yield* Ref.make(1),\n busy: yield* Ref.make(true),\n lock: yield* Semaphore.make(1)\n }\n\n runs.set(run.runId, run)\n yield* launch(run)\n\n return run.runId\n }),\n message: Effect.fnUntraced(function* (\n runId: RunId.RunId,\n text: string\n ): Effect.fn.Return<\"steered\" | \"continued\", Specialist.SpecialistError> {\n const run = runs.get(runId)\n\n if (run === undefined) {\n return yield* new Specialist.SpecialistError({\n reason: \"RunNotFound\",\n message: `Run ${runId} is no longer available. Spawn a new one with the full context.`\n })\n }\n\n return yield* run.lock.withPermit(send(run, text))\n }),\n stop: Effect.fnUntraced(function* (runId: RunId.RunId) {\n if (!(yield* FiberMap.has(working, runId))) {\n return yield* new Specialist.SpecialistError({ reason: \"NotRunning\", message: `Run ${runId} isn't running.` })\n }\n\n yield* FiberMap.remove(working, runId)\n })\n })\n\n // What a run's handler gets: its chat, this runner, and (merged in `once`) the model.\n const services = Context.make(CurrentChat, options.handle).pipe(Context.add(Specialist.Runner, runner))\n\n return runner\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"specialists.js","names":["run","Loop.run","Toolkits.fromToolkitOrEmpty","Loop.TurnApprovals","Telemetry.agentOutput","Telemetry.agentSpan","InputId.fromKey","RunId.RunId","ToolFailure.ToolFailure","Specialist.Runner","ToolFailure.fail","Toolkits.empty","Toolkits.fromWithHandler","Loop.TurnTools","Specialist.SpecialistError","RunId.make"],"sources":["../../src/internal/specialists.ts"],"sourcesContent":["import type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport * as Prompt from \"effect/ai/Prompt\"\nimport * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Fiber from \"effect/Fiber\"\nimport * as FiberMap from \"effect/FiberMap\"\nimport * as FiberSet from \"effect/FiberSet\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Queue from \"effect/Queue\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport * as Stream from \"effect/Stream\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type * as AssistantLoop from \"../AssistantLoop.ts\"\nimport * as Loop from \"./loop.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as InputId from \"../InputId.ts\"\nimport * as RunId from \"../RunId.ts\"\nimport * as Specialist from \"../Specialist.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport { toolMessage, userText } from \"./history.ts\"\nimport * as Telemetry from \"./telemetry.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport type { Writer } from \"./writer.ts\"\n\n/** @internal */\nexport type RunHandler = (run: Specialist.Run) => Stream.Stream<AssistantLoop.Event, unknown, Specialist.RunServices>\n\n/** @internal */\nexport const briefText = (brief: Specialist.Brief): string =>\n [\n `Goal: ${brief.goal}`,\n brief.context && `Context: ${brief.context}`,\n brief.successCriteria && `Done when: ${brief.successCriteria}`,\n brief.report && `Report back: ${brief.report}`,\n brief.limits && `Limits: ${brief.limits}`\n ]\n .filter(Boolean)\n .join(\"\\n\")\n\nconst finalText = (output: AssistantEvent.ModelOutput): string =>\n output.parts\n .flatMap((part) => (part.type === \"text\" ? [part.text] : []))\n .join(\"\")\n .trim()\n\n// The answer is the last text the model wrote.\n/** @internal */\nexport const runBrief = Effect.fnUntraced(\n function* (\n handler: RunHandler,\n options: {\n readonly specialist: Specialist.Any\n readonly runId: RunId.RunId\n readonly brief: Specialist.Brief\n readonly history: Ref.Ref<ReadonlyArray<Prompt.Message>>\n readonly inbox: Queue.Queue<Prompt.Message>\n readonly chatId: Writer[\"chatId\"]\n }\n ) {\n const { history, inbox } = options\n const answer = yield* Ref.make(\"\")\n const add = (messages: ReadonlyArray<Prompt.Message>) => Ref.update(history, (all) => [...all, ...messages])\n\n // Read before every model call, when no tool calls are open: messages sent meanwhile join it now.\n const current = Effect.gen(function* () {\n yield* add(yield* Queue.clear(inbox))\n\n return yield* Ref.get(history)\n })\n\n const run: Specialist.Run = {\n runId: options.runId,\n chatId: options.chatId,\n brief: options.brief,\n history: current,\n // A run can't wait for the user's approval: a call that needs it fails, and the run reads why.\n loop: (loop) =>\n Loop.run({\n instructions: loop.instructions,\n toolkit: Toolkits.fromToolkitOrEmpty(loop.toolkit),\n history: current,\n maxSteps: loop.maxSteps ?? 8\n }).pipe(Stream.provideService(Loop.TurnApprovals, Option.none()))\n }\n\n // A run lives in memory, so there's nothing to run again after a crash: `ToolStarted` isn't kept.\n const record = Effect.fnUntraced(function* (event: AssistantLoop.Event) {\n if (Predicate.isTagged(event, \"ToolStarted\") || Predicate.isTagged(event, \"ApprovalRequested\")) return\n\n if (Predicate.isTagged(event, \"ToolResult\")) return yield* add([toolMessage(event)])\n\n yield* add(Prompt.fromResponseParts(event.parts).content)\n const text = finalText(event)\n\n if (text.length > 0) yield* Ref.set(answer, text)\n })\n\n yield* Stream.runForEach(handler(run), record)\n\n const text = yield* Ref.get(answer)\n\n yield* Telemetry.agentOutput(text)\n\n return text\n },\n (effect, _handler, options) => effect.pipe(Telemetry.agentSpan(options.specialist.name, { chatId: options.chatId }))\n)\n\ntype Outcome =\n | { readonly status: \"answered\"; readonly report: string }\n | { readonly status: \"failed\"; readonly error: string }\n | { readonly status: \"stopped\" | \"cut_off\" }\n\n// Only `stop` interrupts a brief's loop alone; a failure or a defect failed it.\nconst unanswered = (cause: Cause.Cause<unknown>): Outcome =>\n Cause.hasInterruptsOnly(cause) ? { status: \"stopped\" } : { status: \"failed\", error: String(Cause.squash(cause)) }\n\n// The id is fixed per run and brief, so a replay is stored once.\n/** @internal */\nexport const finished = (\n specialist: string,\n run: { readonly runId: RunId.RunId; readonly brief: number; readonly goal: string },\n outcome: Outcome\n): AssistantEvent.SpecialistFinished =>\n AssistantEvent.SpecialistFinished.make({\n id: InputId.fromKey(`${run.runId}:${run.brief}`),\n runId: run.runId,\n specialist,\n brief: run.brief,\n goal: run.goal,\n ...outcome\n })\n\nconst RunIdInput = RunId.RunId.annotate({ description: \"The runId spawn_specialist returned.\" })\n\nconst RunStatus = <const Status extends string>(status: ReadonlyArray<Status>) =>\n Schema.Struct({ runId: RunId.RunId, status: Schema.Literals(status) })\n\nconst MessageSpecialist = Tool.make(\"message_specialist\", {\n description:\n \"Send more to one of your specialists: new instructions for a running one (it reads them at its next step), or a follow-up for a finished one (it continues with what it already found).\",\n parameters: Schema.Struct({ runId: RunIdInput, message: Schema.String }),\n success: RunStatus([\"steered\", \"continued\"]),\n failure: ToolFailure.ToolFailure,\n dependencies: [Specialist.Runner]\n})\n\nconst StopSpecialist = Tool.make(\"stop_specialist\", {\n description:\n \"Stop one of your running specialists, for example when the user changed their mind. It posts no answer.\",\n parameters: Schema.Struct({ runId: RunIdInput }),\n success: RunStatus([\"stopped\"]),\n failure: ToolFailure.ToolFailure,\n dependencies: [Specialist.Runner]\n})\n\nconst refused = <A, R>(effect: Effect.Effect<A, Specialist.SpecialistError, R>) =>\n Effect.catch(effect, (error) => ToolFailure.fail(error.message, { reason: error.reason }))\n\n/** @internal */\nexport const toolkit = Effect.fnUntraced(function* (\n specialists: ReadonlyArray<Specialist.Any>\n): Effect.fn.Return<Toolkits.Tools<Specialist.Runner>> {\n if (specialists.length === 0) return Toolkits.empty\n\n const catalog = specialists.map((specialist) => `- ${specialist.name}: ${specialist.description}`).join(\"\\n\")\n\n const SpawnSpecialist = Tool.make(\"spawn_specialist\", {\n description: `Start a specialist on a job. It works in the background and its report arrives as a <specialist> input, often within seconds. Write a self-contained brief: it can't see this chat. For a quick question, just call wait; for slow work, tell the user in one line first.\\nPass one of these as specialist:\\n${catalog}`,\n parameters: Schema.Struct({\n specialist: Schema.Literals(specialists.map((specialist) => specialist.name)).annotate({\n description: \"The specialist to start, by name.\"\n }),\n ...Specialist.Brief.fields\n }),\n success: RunStatus([\"started\"]),\n failure: ToolFailure.ToolFailure,\n dependencies: [Specialist.Runner]\n })\n\n const SpecialistTools = Toolkit.make(SpawnSpecialist, MessageSpecialist, StopSpecialist)\n\n const handlers = yield* SpecialistTools.toHandlers({\n spawn_specialist: Effect.fnUntraced(function* ({ specialist, ...brief }) {\n const runId = yield* refused(Specialist.Runner.use((runner) => runner.spawn({ specialist, brief })))\n\n return { runId, status: \"started\" }\n }),\n message_specialist: Effect.fnUntraced(function* ({ runId, message }) {\n const status = yield* refused(Specialist.Runner.use((runner) => runner.message(runId, message)))\n\n return { runId, status }\n }),\n stop_specialist: Effect.fnUntraced(function* ({ runId }) {\n yield* refused(Specialist.Runner.use((runner) => runner.stop(runId)))\n\n return { runId, status: \"stopped\" }\n })\n })\n\n return Toolkits.fromWithHandler(yield* Effect.provideContext(SpecialistTools, handlers))\n})\n\n// How long a finished run stays available for follow-ups.\nconst warm = \"15 minutes\"\n\ninterface Run {\n readonly runId: RunId.RunId\n readonly specialist: Specialist.Any\n readonly handler: RunHandler\n readonly goal: string\n readonly brief: Specialist.Brief\n readonly history: Ref.Ref<ReadonlyArray<Prompt.Message>>\n // Messages sent while it works; the loop reads them at its next step.\n readonly inbox: Queue.Queue<Prompt.Message>\n // Counts follow-ups too; numbers the reports.\n readonly briefs: Ref.Ref<number>\n // Read and changed under `lock`.\n readonly busy: Ref.Ref<boolean>\n // Orders a message against the run finishing, so none is dropped or starts a second loop.\n readonly lock: Semaphore.Semaphore\n}\n\n/** @internal */\nexport const makeRunner = Effect.fnUntraced(function* (options: {\n readonly chat: Writer\n readonly handle: CurrentChat[\"Service\"]\n readonly specialists: ReadonlyArray<{\n readonly specialist: Specialist.Any\n readonly handler: RunHandler\n }>\n readonly model: Context.Context<LanguageModel.LanguageModel>\n // An interruption keeps the hold.\n readonly holding: (effect: Effect.Effect<void>) => Effect.Effect<void>\n readonly report: (finished: AssistantEvent.SpecialistFinished) => Effect.Effect<void>\n}) {\n const { chat } = options\n const runs = new Map<string, Run>()\n const byName = new Map(options.specialists.map((entry) => [entry.specialist.name, entry]))\n // The brief loops working now, and the finished runs' countdowns to being forgotten.\n const working = yield* FiberMap.make<string, Outcome>()\n const forgetting = yield* FiberMap.make<string>()\n const background = yield* FiberSet.make()\n\n const report = Effect.fnUntraced(function* (run: Run, outcome: Outcome) {\n const brief = yield* Ref.get(run.briefs)\n\n yield* options.report(finished(run.specialist.name, { runId: run.runId, brief, goal: run.goal }, outcome))\n })\n\n const once = (run: Run) =>\n runBrief(run.handler, {\n specialist: run.specialist,\n runId: run.runId,\n brief: run.brief,\n history: run.history,\n inbox: run.inbox,\n chatId: chat.chatId\n }).pipe(\n Effect.provideContext(Context.merge(options.model, services)),\n // Spawned from a turn, a run would inherit the turn's own tools: a run can't message the user.\n Effect.provideService(Loop.TurnTools, Option.none()),\n Effect.matchCause({\n onSuccess: (answer): Outcome => ({ status: \"answered\", report: answer }),\n onFailure: unanswered\n })\n )\n\n // A message that arrived after the last read: keep working before reporting.\n const untilCaughtUp = Effect.fnUntraced(function* (run: Run) {\n let outcome = yield* once(run)\n\n while ((yield* Queue.size(run.inbox)) > 0) outcome = yield* once(run)\n\n return outcome\n })\n\n const work = Effect.fnUntraced(\n function* (run: Run) {\n yield* chat.append([\n AssistantEvent.SpecialistStarted.make({\n runId: run.runId,\n specialist: run.specialist.name,\n goal: run.goal\n })\n ])\n\n let finished = false\n\n while (!finished) {\n const exit = yield* Fiber.await(yield* FiberMap.run(working, run.runId, untilCaughtUp(run)))\n const outcome = Exit.isSuccess(exit) ? exit.value : unanswered(exit.cause)\n\n finished = yield* run.lock.withPermit(\n Effect.gen(function* () {\n // A message steered in after the loop's last read: work on it before reporting.\n if (outcome.status !== \"stopped\" && (yield* Queue.size(run.inbox)) > 0) return false\n\n yield* report(run, outcome)\n yield* Ref.set(run.busy, false)\n yield* FiberMap.run(\n forgetting,\n run.runId,\n Effect.gen(function* () {\n yield* Effect.sleep(warm)\n runs.delete(run.runId)\n })\n )\n\n return true\n })\n )\n }\n },\n // A shutdown interrupts the whole run: no turn can start now, so the report waits for the chat's next start.\n (effect, run) => Effect.onInterrupt(effect, () => report(run, { status: \"cut_off\" }))\n )\n\n const launch = Effect.fnUntraced(function* (run: Run) {\n yield* FiberMap.remove(forgetting, run.runId)\n yield* FiberSet.run(background, options.holding(work(run)))\n })\n\n // Under the run's lock.\n const send = Effect.fnUntraced(function* (run: Run, text: string): Effect.fn.Return<\"steered\" | \"continued\"> {\n yield* Ref.update(run.briefs, (n) => n + 1)\n\n if (yield* Ref.get(run.busy)) {\n yield* Queue.offer(run.inbox, userText(text))\n\n return \"steered\"\n }\n\n yield* Ref.update(run.history, (all) => [...all, userText(text)])\n yield* Ref.set(run.busy, true)\n yield* launch(run)\n\n return \"continued\"\n })\n\n const runner = Specialist.Runner.of({\n spawn: Effect.fnUntraced(function* (request: Specialist.SpawnRequest) {\n const found = byName.get(request.specialist)\n\n if (found === undefined) {\n return yield* new Specialist.SpecialistError({\n reason: \"UnknownSpecialist\",\n message: `No specialist named ${request.specialist}.`\n })\n }\n\n const run: Run = {\n runId: yield* RunId.make(),\n specialist: found.specialist,\n handler: found.handler,\n goal: request.brief.goal,\n brief: request.brief,\n history: yield* Ref.make<ReadonlyArray<Prompt.Message>>([userText(briefText(request.brief))]),\n inbox: yield* Queue.unbounded<Prompt.Message>(),\n briefs: yield* Ref.make(1),\n busy: yield* Ref.make(true),\n lock: yield* Semaphore.make(1)\n }\n\n runs.set(run.runId, run)\n yield* launch(run)\n\n return run.runId\n }),\n message: Effect.fnUntraced(function* (\n runId: RunId.RunId,\n text: string\n ): Effect.fn.Return<\"steered\" | \"continued\", Specialist.SpecialistError> {\n const run = runs.get(runId)\n\n if (run === undefined) {\n return yield* new Specialist.SpecialistError({\n reason: \"RunNotFound\",\n message: `Run ${runId} is no longer available. Spawn a new one with the full context.`\n })\n }\n\n return yield* run.lock.withPermit(send(run, text))\n }),\n stop: Effect.fnUntraced(function* (runId: RunId.RunId) {\n if (!(yield* FiberMap.has(working, runId))) {\n return yield* new Specialist.SpecialistError({ reason: \"NotRunning\", message: `Run ${runId} isn't running.` })\n }\n\n yield* FiberMap.remove(working, runId)\n })\n })\n\n // What a run's handler gets: its chat, this runner, and (merged in `once`) the model.\n const services = Context.make(CurrentChat, options.handle).pipe(Context.add(Specialist.Runner, runner))\n\n return runner\n})\n"],"mappings":"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"}
@@ -3,10 +3,11 @@ import { AssistantToolCall } from "../AssistantToolCall.js";
3
3
  import * as Context from "effect/Context";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Schema from "effect/Schema";
6
+ import * as Prompt from "effect/ai/Prompt";
6
7
  import * as Tool from "effect/ai/Tool";
7
8
  import * as Toolkit from "effect/ai/Toolkit";
8
9
  declare namespace toolkit_d_exports {
9
- export { CallResult, ToolServices, Tools, duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval, needsApprovalMessage, provide, runNative, runOnly, withContext };
10
+ export { CallResult, ToolServices, Tools, duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval, provide, runNative, runOnly };
10
11
  }
11
12
  /** @internal */
12
13
  export interface CallResult {
@@ -18,16 +19,15 @@ export interface CallResult {
18
19
  export interface Tools<out R = never> {
19
20
  readonly definitions: ReadonlyArray<Tool.Any>;
20
21
  readonly run: (call: ToolCallPart) => Effect.Effect<CallResult, never, R>;
22
+ readonly needsApproval: (call: ToolCallPart, history: ReadonlyArray<Prompt.Message>) => Effect.Effect<boolean, never, R>;
21
23
  }
22
24
  /** @internal */
23
- export type ToolServices<T extends Record<string, Tool.Any>> = Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall>;
25
+ export type ToolServices<T extends Record<string, Tool.Any>> = Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall> | T[keyof T]["parametersSchema"]["DecodingServices"];
24
26
  /** @internal */
25
27
  export declare const failed: (message: string, reason: string) => CallResult;
26
28
  /** @internal */
27
29
  export declare const needingApproval: (definitions: ReadonlyArray<Tool.Any>) => ReadonlyArray<string>;
28
30
  /** @internal */
29
- export declare const needsApprovalMessage: (names: ReadonlyArray<string>) => string;
30
- /** @internal */
31
31
  export declare const runNative: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.WithHandler<T>, call: {
32
32
  readonly type: "tool-call";
33
33
  readonly id: string;
@@ -38,14 +38,12 @@ export declare const runNative: <T extends Record<string, Tool.Any>>(toolkit: To
38
38
  readonly [x: string]: Schema.Json;
39
39
  };
40
40
  readonly name: string;
41
- }) => Effect.Effect<CallResult, never, Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall>>;
41
+ }) => Effect.Effect<CallResult, never, ToolServices<T>>;
42
42
  /** @internal */
43
43
  export declare const fromWithHandler: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.WithHandler<T>) => Tools<ToolServices<T>>;
44
44
  /** @internal */
45
45
  export declare const fromToolkit: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>) => Tools<ToolServices<T> | Tool.HandlersFor<T>>;
46
46
  /** @internal */
47
- export declare const withContext: <T extends Record<string, Tool.Any>, R>(toolkit: Toolkit.Toolkit<T>, handlers: (context: Context.Context<R>) => Toolkit.HandlersFrom<T>) => Tools<R | ToolServices<T>>;
48
- /** @internal */
49
47
  export declare const fromToolkitOrEmpty: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T> | undefined) => Tools<ToolServices<T> | Tool.HandlersFor<T>>;
50
48
  /** @internal */
51
49
  export declare const empty: Tools;
@@ -1 +1 @@
1
- {"version":3,"file":"toolkit.d.ts","names":[],"sources":["../../src/internal/toolkit.ts"],"mappings":";;;;;;;;;;;iBAciB;WACN,SAAS;WACT,QAAQ,OAAO;WACf;;;iBAKM,UAAU;WAChB,aAAa,cAAc,KAAK;WAChC,MAAM,MAAM,iBAAgC,OAAO,OAAO,mBAAmB;;;YAI5E,aAAa,UAAU,eAAe,KAAK,QAAQ,QAC7D,KAAK,gBAAgB,QAAQ,KAC7B;;qBAMW,SAAM,iBAAmB,mBAAmB;;qBA6B5C,kBAAe,aAAiB,cAAc,KAAK,SAAO;;qBAI1D,uBAAoB,OAAW;;qBAU/B,YAAyC,UAAU,eAAe,KAAK,MAAI,SAAA,QAAA,YAAA,IAAA;;;;;;;;;;MAiCtF,OAAA,OAAA,mBAAA,QAAA,KAAA,gBAAA,QAAA,KAAA;;qBAGW,kBAAmB,UAAU,eAAe,KAAK,MAAI,SACvD,QAAQ,YAAY,OAC5B,MAAM,aAAa;;qBAMT,cAAe,UAAU,eAAe,KAAK,MAAI,SACnD,QAAQ,QAAQ,OACxB,MAAM,aAAa,KAAK,KAAK,YAAY;;qBAS/B,cAAe,UAAU,eAAe,KAAK,MAAM,GAAC,SACtD,QAAQ,QAAQ,IAAE,WAChB,SAAS,QAAQ,QAAQ,OAAO,QAAQ,aAAa,OAC/D,MAAM,IAAI,aAAa;;qBAWb,qBAAsB,UAAU,eAAe,KAAK,MAAI,SAC1D,QAAQ,QAAQ,mBACxB,MAAM,aAAa,KAAK,KAAK,YAAY;;qBAG/B,OAAO;;qBAMP,QAAS,GAAG,GAAC,MAAQ,MAAM,IAAE,MAAQ,MAAM,OAAK,MAAM,IAAI;;qBAW1D,UAAW,GAAC,OAAS,MAAM,IAAE,SAAW,QAAQ,QAAQ,OAAK;;qBAM7D,iBAAc,OAAW,mBAAiB;;qBAQ1C,UAAW,GAAC,OAAS,MAAM,IAAE,OAAS,uBAAqB,oBAAoB,MAAM"}
1
+ {"version":3,"file":"toolkit.d.ts","names":[],"sources":["../../src/internal/toolkit.ts"],"mappings":";;;;;;;;;;;;iBAgBiB;WACN,SAAS;WACT,QAAQ,OAAO;WACf;;;iBAKM,UAAU;WAChB,aAAa,cAAc,KAAK;WAChC,MAAM,MAAM,iBAAgC,OAAO,OAAO,mBAAmB;WAE7E,gBACP,MAAM,cACN,SAAS,cAAc,OAAO,aAC3B,OAAO,uBAAuB;;;YAIzB,aAAa,UAAU,eAAe,KAAK,QACnD,QAAQ,KAAK,gBAAgB,QAAQ,KAAK,qBAE1C,QAAQ;;qBAKC,SAAM,iBAAmB,mBAAmB;;qBA2B5C,kBAAe,aAAiB,cAAc,KAAK,SAAO;;qBAiC1D,YAAyC,UAAU,eAAe,KAAK,MAAI,SAAA,QAAA,YAAA,IAAA;;;;;;;;;;MAiCtF,OAAA,OAAA,mBAAA,aAAA;;qBAGW,kBAAmB,UAAU,eAAe,KAAK,MAAI,SACvD,QAAQ,YAAY,OAC5B,MAAM,aAAa;;qBAOT,cAAe,UAAU,eAAe,KAAK,MAAI,SACnD,QAAQ,QAAQ,OACxB,MAAM,aAAa,KAAK,KAAK,YAAY;;qBAS/B,qBAAsB,UAAU,eAAe,KAAK,MAAI,SAC1D,QAAQ,QAAQ,mBACxB,MAAM,aAAa,KAAK,KAAK,YAAY;;qBAG/B,OAAO;;qBAOP,QAAS,GAAG,GAAC,MAAQ,MAAM,IAAE,MAAQ,MAAM,OAAK,MAAM,IAAI;;qBAa1D,UAAW,GAAC,OAAS,MAAM,IAAE,SAAW,QAAQ,QAAQ,OAAK;;qBAO7D,iBAAc,OAAW,mBAAiB;;qBAQ1C,UAAW,GAAC,OAAS,MAAM,IAAE,OAAS,uBAAqB,oBAAoB,MAAM"}
@@ -2,6 +2,7 @@ import { toJson } from "./schema.js";
2
2
  import { AssistantToolCall } from "../AssistantToolCall.js";
3
3
  import { ToolFailure } from "../ToolFailure.js";
4
4
  import * as Effect from "effect/Effect";
5
+ import * as Predicate from "effect/Predicate";
5
6
  import * as Schema from "effect/Schema";
6
7
  import * as Stream from "effect/Stream";
7
8
  import * as Option from "effect/Option";
@@ -27,8 +28,20 @@ const failure = (result) => ({
27
28
  const hasTool = (toolkit, name) => Object.hasOwn(toolkit.tools, name);
28
29
  /** @internal */
29
30
  const needingApproval = (definitions) => definitions.flatMap((tool) => tool.needsApproval === void 0 || tool.needsApproval === false ? [] : [tool.name]);
30
- /** @internal */
31
- const needsApprovalMessage = (names) => `${names.join(", ")} ${names.length === 1 ? "sets" : "set"} needsApproval, and effect-assistant doesn't run tools that wait for approval. Remove needsApproval: let the model call the tool with final values, or show a block whose button calls your app's API, which does the work and submits an app event that wakes the chat.`;
31
+ const toolNamed = (toolkit, name) => Object.hasOwn(toolkit.tools, name) ? toolkit.tools[name] : void 0;
32
+ const needsApprovalNative = Effect.fnUntraced(function* (toolkit, call, history) {
33
+ const tool = toolNamed(toolkit, call.name);
34
+ const needs = tool?.needsApproval;
35
+ if (tool === void 0 || needs === void 0 || Predicate.isBoolean(needs)) return needs === true;
36
+ return yield* Effect.gen(function* () {
37
+ const params = yield* Schema.decodeUnknownEffect(tool.parametersSchema)(call.params);
38
+ const decided = needs(params, {
39
+ toolCallId: call.id,
40
+ messages: history
41
+ });
42
+ return Effect.isEffect(decided) ? yield* decided : decided;
43
+ }).pipe(Effect.orElseSucceed(() => false));
44
+ });
32
45
  const isParams = (params) => params !== void 0;
33
46
  /** @internal */
34
47
  const runNative = Effect.fnUntraced(function* (toolkit, call) {
@@ -54,43 +67,39 @@ const runNative = Effect.fnUntraced(function* (toolkit, call) {
54
67
  /** @internal */
55
68
  const fromWithHandler = (toolkit) => ({
56
69
  definitions: Object.values(toolkit.tools),
57
- run: (call) => runNative(toolkit, call)
70
+ run: (call) => runNative(toolkit, call),
71
+ needsApproval: (call, history) => needsApprovalNative(toolkit, call, history)
58
72
  });
59
73
  /** @internal */
60
74
  const fromToolkit = (toolkit) => ({
61
75
  definitions: Object.values(toolkit.tools),
62
76
  run: Effect.fnUntraced(function* (call) {
63
77
  return yield* runNative(yield* toolkit, call);
64
- })
65
- });
66
- /** @internal */
67
- const withContext = (toolkit, handlers) => ({
68
- definitions: Object.values(toolkit.tools),
69
- run: Effect.fnUntraced(function* (call) {
70
- const context = yield* Effect.context();
71
- const resolved = yield* Effect.provideContext(toolkit, yield* toolkit.toHandlers(handlers(context)));
72
- return yield* runNative(resolved, call);
73
- })
78
+ }),
79
+ needsApproval: (call, history) => needsApprovalNative(toolkit, call, history)
74
80
  });
75
81
  /** @internal */
76
82
  const fromToolkitOrEmpty = (toolkit) => toolkit === void 0 ? empty : fromToolkit(toolkit);
77
83
  /** @internal */
78
84
  const empty = {
79
85
  definitions: [],
80
- run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, "ToolNotFoundError"))
86
+ run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, "ToolNotFoundError")),
87
+ needsApproval: () => Effect.succeed(false)
81
88
  };
82
89
  /** @internal */
83
90
  const merge = (self, that) => {
84
91
  const inSelf = (call) => self.definitions.some((tool) => tool.name === call.name);
85
92
  return {
86
93
  definitions: [...self.definitions, ...that.definitions],
87
- run: (call) => inSelf(call) ? self.run(call) : that.run(call)
94
+ run: (call) => inSelf(call) ? self.run(call) : that.run(call),
95
+ needsApproval: (call, history) => inSelf(call) ? self.needsApproval(call, history) : that.needsApproval(call, history)
88
96
  };
89
97
  };
90
98
  /** @internal */
91
99
  const provide = (tools, context) => ({
92
100
  definitions: tools.definitions,
93
- run: (call) => Effect.provideContext(tools.run(call), context)
101
+ run: (call) => Effect.provideContext(tools.run(call), context),
102
+ needsApproval: (call, history) => Effect.provideContext(tools.needsApproval(call, history), context)
94
103
  });
95
104
  /** @internal */
96
105
  const duplicateNames = (tools) => {
@@ -100,9 +109,10 @@ const duplicateNames = (tools) => {
100
109
  /** @internal */
101
110
  const runOnly = (tools, names, message) => ({
102
111
  definitions: tools.definitions,
103
- run: (call) => names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow"))
112
+ run: (call) => names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow")),
113
+ needsApproval: () => Effect.succeed(false)
104
114
  });
105
115
  //#endregion
106
- export { duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval, needsApprovalMessage, provide, runNative, runOnly, withContext };
116
+ export { duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval, provide, runNative, runOnly };
107
117
 
108
118
  //# sourceMappingURL=toolkit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"toolkit.js","names":["ToolFailure.ToolFailure"],"sources":["../../src/internal/toolkit.ts"],"sourcesContent":["import * as AiError from \"effect/ai/AiError\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Toolkit from \"effect/ai/Toolkit\"\nimport type * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { AssistantToolCall } from \"../AssistantToolCall.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport { toJson } from \"./schema.ts\"\n\n/** @internal */\nexport interface CallResult {\n readonly outcome: AssistantEvent.ToolOutcome\n readonly result: Schema.Json\n readonly reason?: string | undefined\n}\n\n// Apps give the loop native toolkits; these are what the loop makes of them, merged with its own.\n/** @internal */\nexport interface Tools<out R = never> {\n readonly definitions: ReadonlyArray<Tool.Any>\n readonly run: (call: AssistantEvent.ToolCallPart) => Effect.Effect<CallResult, never, R>\n}\n\n/** @internal */\nexport type ToolServices<T extends Record<string, Tool.Any>> = Exclude<\n Tool.HandlerServices<T[keyof T]>,\n AssistantToolCall\n>\n\nconst encodeFailure = Schema.encodeSync(ToolFailure.ToolFailure)\n\n/** @internal */\nexport const failed = (message: string, reason: string): CallResult => ({\n outcome: \"failure\",\n result: toJson(encodeFailure(new ToolFailure.ToolFailure({ message, reason }))),\n reason\n})\n\nconst decodeReason = Schema.decodeUnknownOption(Schema.Struct({ reason: Schema.String }))\n\nconst decodeTag = Schema.decodeUnknownOption(Schema.Struct({ _tag: Schema.String }))\n\nconst failure = (result: Schema.Json): CallResult => ({\n outcome: \"failure\",\n result,\n reason: Option.getOrUndefined(\n Option.orElse(\n Option.map(decodeReason(result), ({ reason }) => reason),\n () => Option.map(decodeTag(result), ({ _tag }) => _tag)\n )\n )\n})\n\nconst hasTool = <T extends Record<string, Tool.Any>>(\n toolkit: { readonly tools: T },\n name: PropertyKey\n): name is keyof T => Object.hasOwn(toolkit.tools, name)\n\n// Approval would park a call until the user decides, which this library doesn't do: the model calls a tool with\n// final values, and a user's own commitment goes through the app's API from a block.\n/** @internal */\nexport const needingApproval = (definitions: ReadonlyArray<Tool.Any>): ReadonlyArray<string> =>\n definitions.flatMap((tool) => (tool.needsApproval === undefined || tool.needsApproval === false ? [] : [tool.name]))\n\n/** @internal */\nexport const needsApprovalMessage = (names: ReadonlyArray<string>): string =>\n `${names.join(\", \")} ${names.length === 1 ? \"sets\" : \"set\"} needsApproval, and effect-assistant doesn't run tools that wait for approval. Remove needsApproval: let the model call the tool with final values, or show a block whose button calls your app's API, which does the work and submits an app event that wakes the chat.`\n\n// The toolkit decodes the params with the tool's schema before its handler sees them, and a call whose params don't\n// fit fails with a validation error the model reads: the model's JSON is what `handle` takes.\nconst isParams = <T>(params: unknown): params is Tool.ParametersEncoded<T> => params !== undefined\n\n// Every failure is the call's result, for the model to read. With `failureMode: \"return\"` the toolkit encodes the\n// tool's own failure with its schema first.\n/** @internal */\nexport const runNative = Effect.fnUntraced(function* <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.WithHandler<T>,\n call: AssistantEvent.ToolCallPart\n): Effect.fn.Return<CallResult, never, ToolServices<T>> {\n const name: PropertyKey = call.name\n\n if (!hasTool(toolkit, name)) return failed(`No tool named ${call.name}.`, \"ToolNotFoundError\")\n\n const params: unknown = call.params\n\n if (!isParams<T[keyof T]>(params)) {\n return failed(`Invalid parameters for ${call.name}.`, \"ToolParameterValidationError\")\n }\n\n return yield* Effect.gen(function* () {\n const results = yield* toolkit.handle<keyof T>(name, params, call.id)\n const last = yield* Stream.runLast(Stream.filter(results, (result) => !result.preliminary))\n\n return Option.match(last, {\n onNone: () => failed(`${call.name} returned no result.`, \"NoResult\"),\n onSome: (result): CallResult =>\n result.isFailure\n ? failure(toJson(result.encodedResult))\n : { outcome: \"success\", result: toJson(result.encodedResult) }\n })\n }).pipe(\n Effect.catch((error) =>\n Effect.succeed(\n AiError.isAiError(error) ? failed(error.reason.message, error.reason._tag) : failure(toJson(error))\n )\n ),\n Effect.provideService(AssistantToolCall, { callId: call.id, tool: call.name })\n )\n})\n\n/** @internal */\nexport const fromWithHandler = <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.WithHandler<T>\n): Tools<ToolServices<T>> => ({\n definitions: Object.values(toolkit.tools),\n run: (call) => runNative(toolkit, call)\n})\n\n/** @internal */\nexport const fromToolkit = <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.Toolkit<T>\n): Tools<ToolServices<T> | Tool.HandlersFor<T>> => ({\n definitions: Object.values(toolkit.tools),\n run: Effect.fnUntraced(function* (call) {\n return yield* runNative(yield* toolkit, call)\n })\n})\n\n// The handlers use services of the loop they run in, so they're made for each call, with its context.\n/** @internal */\nexport const withContext = <T extends Record<string, Tool.Any>, R>(\n toolkit: Toolkit.Toolkit<T>,\n handlers: (context: Context.Context<R>) => Toolkit.HandlersFrom<T>\n): Tools<R | ToolServices<T>> => ({\n definitions: Object.values(toolkit.tools),\n run: Effect.fnUntraced(function* (call) {\n const context = yield* Effect.context<R>()\n const resolved = yield* Effect.provideContext(toolkit, yield* toolkit.toHandlers(handlers(context)))\n\n return yield* runNative(resolved, call)\n })\n})\n\n/** @internal */\nexport const fromToolkitOrEmpty = <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.Toolkit<T> | undefined\n): Tools<ToolServices<T> | Tool.HandlersFor<T>> => (toolkit === undefined ? empty : fromToolkit(toolkit))\n\n/** @internal */\nexport const empty: Tools = {\n definitions: [],\n run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, \"ToolNotFoundError\"))\n}\n\n/** @internal */\nexport const merge = <A, B>(self: Tools<A>, that: Tools<B>): Tools<A | B> => {\n const inSelf = (call: AssistantEvent.ToolCallPart) => self.definitions.some((tool) => tool.name === call.name)\n\n return {\n definitions: [...self.definitions, ...that.definitions],\n run: (call) => (inSelf(call) ? self.run(call) : that.run(call))\n }\n}\n\n// A turn's own services, for a loop the app writes.\n/** @internal */\nexport const provide = <R>(tools: Tools<R>, context: Context.Context<R>): Tools => ({\n definitions: tools.definitions,\n run: (call) => Effect.provideContext(tools.run(call), context)\n})\n\n/** @internal */\nexport const duplicateNames = (tools: Tools<unknown>): ReadonlyArray<string> => {\n const names = tools.definitions.map((tool) => tool.name)\n\n return names.filter((name, i) => names.indexOf(name) !== i)\n}\n\n// The same tools are offered, so the prompt doesn't change.\n/** @internal */\nexport const runOnly = <R>(tools: Tools<R>, names: ReadonlyArray<string>, message: string): Tools<R> => ({\n definitions: tools.definitions,\n run: (call) => (names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, \"NotNow\")))\n})\n"],"mappings":";;;;;;;;;AAiCA,MAAM,gBAAgB,OAAO,WAAWA,WAAuB;;AAG/D,MAAa,UAAU,SAAiB,YAAgC;CACtE,SAAS;CACT,QAAQ,OAAO,cAAc,IAAIA,YAAwB;EAAE;EAAS;CAAO,CAAC,CAAC,CAAC;CAC9E;AACF;AAEA,MAAM,eAAe,OAAO,oBAAoB,OAAO,OAAO,EAAE,QAAQ,OAAO,OAAO,CAAC,CAAC;AAExF,MAAM,YAAY,OAAO,oBAAoB,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,CAAC,CAAC;AAEnF,MAAM,WAAW,YAAqC;CACpD,SAAS;CACT;CACA,QAAQ,OAAO,eACb,OAAO,OACL,OAAO,IAAI,aAAa,MAAM,IAAI,EAAE,aAAa,MAAM,SACjD,OAAO,IAAI,UAAU,MAAM,IAAI,EAAE,WAAW,IAAI,CACxD,CACF;AACF;AAEA,MAAM,WACJ,SACA,SACoB,OAAO,OAAO,QAAQ,OAAO,IAAI;;AAKvD,MAAa,mBAAmB,gBAC9B,YAAY,SAAS,SAAU,KAAK,kBAAkB,KAAA,KAAa,KAAK,kBAAkB,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,CAAE;;AAGrH,MAAa,wBAAwB,UACnC,GAAG,MAAM,KAAK,IAAI,EAAE,GAAG,MAAM,WAAW,IAAI,SAAS,MAAM;AAI7D,MAAM,YAAe,WAAyD,WAAW,KAAA;;AAKzF,MAAa,YAAY,OAAO,WAAW,WACzC,SACA,MACsD;CACtD,MAAM,OAAoB,KAAK;CAE/B,IAAI,CAAC,QAAQ,SAAS,IAAI,GAAG,OAAO,OAAO,iBAAiB,KAAK,KAAK,IAAI,mBAAmB;CAE7F,MAAM,SAAkB,KAAK;CAE7B,IAAI,CAAC,SAAqB,MAAM,GAC9B,OAAO,OAAO,0BAA0B,KAAK,KAAK,IAAI,8BAA8B;CAGtF,OAAO,OAAO,OAAO,IAAI,aAAa;EACpC,MAAM,UAAU,OAAO,QAAQ,OAAgB,MAAM,QAAQ,KAAK,EAAE;EACpE,MAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,UAAU,WAAW,CAAC,OAAO,WAAW,CAAC;EAE1F,OAAO,OAAO,MAAM,MAAM;GACxB,cAAc,OAAO,GAAG,KAAK,KAAK,uBAAuB,UAAU;GACnE,SAAS,WACP,OAAO,YACH,QAAQ,OAAO,OAAO,aAAa,CAAC,IACpC;IAAE,SAAS;IAAW,QAAQ,OAAO,OAAO,aAAa;GAAE;EACnE,CAAC;CACH,CAAC,CAAC,CAAC,KACD,OAAO,OAAO,UACZ,OAAO,QACL,QAAQ,UAAU,KAAK,IAAI,OAAO,MAAM,OAAO,SAAS,MAAM,OAAO,IAAI,IAAI,QAAQ,OAAO,KAAK,CAAC,CACpG,CACF,GACA,OAAO,eAAe,mBAAmB;EAAE,QAAQ,KAAK;EAAI,MAAM,KAAK;CAAK,CAAC,CAC/E;AACF,CAAC;;AAGD,MAAa,mBACX,aAC4B;CAC5B,aAAa,OAAO,OAAO,QAAQ,KAAK;CACxC,MAAM,SAAS,UAAU,SAAS,IAAI;AACxC;;AAGA,MAAa,eACX,aACkD;CAClD,aAAa,OAAO,OAAO,QAAQ,KAAK;CACxC,KAAK,OAAO,WAAW,WAAW,MAAM;EACtC,OAAO,OAAO,UAAU,OAAO,SAAS,IAAI;CAC9C,CAAC;AACH;;AAIA,MAAa,eACX,SACA,cACgC;CAChC,aAAa,OAAO,OAAO,QAAQ,KAAK;CACxC,KAAK,OAAO,WAAW,WAAW,MAAM;EACtC,MAAM,UAAU,OAAO,OAAO,QAAW;EACzC,MAAM,WAAW,OAAO,OAAO,eAAe,SAAS,OAAO,QAAQ,WAAW,SAAS,OAAO,CAAC,CAAC;EAEnG,OAAO,OAAO,UAAU,UAAU,IAAI;CACxC,CAAC;AACH;;AAGA,MAAa,sBACX,YACkD,YAAY,KAAA,IAAY,QAAQ,YAAY,OAAO;;AAGvG,MAAa,QAAe;CAC1B,aAAa,CAAC;CACd,MAAM,SAAS,OAAO,QAAQ,OAAO,iBAAiB,KAAK,KAAK,IAAI,mBAAmB,CAAC;AAC1F;;AAGA,MAAa,SAAe,MAAgB,SAAiC;CAC3E,MAAM,UAAU,SAAsC,KAAK,YAAY,MAAM,SAAS,KAAK,SAAS,KAAK,IAAI;CAE7G,OAAO;EACL,aAAa,CAAC,GAAG,KAAK,aAAa,GAAG,KAAK,WAAW;EACtD,MAAM,SAAU,OAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI;CAC/D;AACF;;AAIA,MAAa,WAAc,OAAiB,aAAwC;CAClF,aAAa,MAAM;CACnB,MAAM,SAAS,OAAO,eAAe,MAAM,IAAI,IAAI,GAAG,OAAO;AAC/D;;AAGA,MAAa,kBAAkB,UAAiD;CAC9E,MAAM,QAAQ,MAAM,YAAY,KAAK,SAAS,KAAK,IAAI;CAEvD,OAAO,MAAM,QAAQ,MAAM,MAAM,MAAM,QAAQ,IAAI,MAAM,CAAC;AAC5D;;AAIA,MAAa,WAAc,OAAiB,OAA8B,aAA+B;CACvG,aAAa,MAAM;CACnB,MAAM,SAAU,MAAM,SAAS,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,QAAQ,OAAO,SAAS,QAAQ,CAAC;AACxG"}
1
+ {"version":3,"file":"toolkit.js","names":["ToolFailure.ToolFailure"],"sources":["../../src/internal/toolkit.ts"],"sourcesContent":["import * as AiError from \"effect/ai/AiError\"\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Toolkit from \"effect/ai/Toolkit\"\nimport type * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { AssistantToolCall } from \"../AssistantToolCall.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport { toJson } from \"./schema.ts\"\n\n/** @internal */\nexport interface CallResult {\n readonly outcome: AssistantEvent.ToolOutcome\n readonly result: Schema.Json\n readonly reason?: string | undefined\n}\n\n// Apps give the loop native toolkits; these are what the loop makes of them, merged with its own.\n/** @internal */\nexport interface Tools<out R = never> {\n readonly definitions: ReadonlyArray<Tool.Any>\n readonly run: (call: AssistantEvent.ToolCallPart) => Effect.Effect<CallResult, never, R>\n // Whether the call waits for the user's approval instead of running: its tool's `needsApproval`.\n readonly needsApproval: (\n call: AssistantEvent.ToolCallPart,\n history: ReadonlyArray<Prompt.Message>\n ) => Effect.Effect<boolean, never, R>\n}\n\n/** @internal */\nexport type ToolServices<T extends Record<string, Tool.Any>> =\n | Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall>\n // Already in the handlers' services; stated for the loop, which decodes params to ask `needsApproval`.\n | T[keyof T][\"parametersSchema\"][\"DecodingServices\"]\n\nconst encodeFailure = Schema.encodeSync(ToolFailure.ToolFailure)\n\n/** @internal */\nexport const failed = (message: string, reason: string): CallResult => ({\n outcome: \"failure\",\n result: toJson(encodeFailure(new ToolFailure.ToolFailure({ message, reason }))),\n reason\n})\n\nconst decodeReason = Schema.decodeUnknownOption(Schema.Struct({ reason: Schema.String }))\n\nconst decodeTag = Schema.decodeUnknownOption(Schema.Struct({ _tag: Schema.String }))\n\nconst failure = (result: Schema.Json): CallResult => ({\n outcome: \"failure\",\n result,\n reason: Option.getOrUndefined(\n Option.orElse(\n Option.map(decodeReason(result), ({ reason }) => reason),\n () => Option.map(decodeTag(result), ({ _tag }) => _tag)\n )\n )\n})\n\nconst hasTool = <T extends Record<string, Tool.Any>>(\n toolkit: { readonly tools: T },\n name: PropertyKey\n): name is keyof T => Object.hasOwn(toolkit.tools, name)\n\n/** @internal */\nexport const needingApproval = (definitions: ReadonlyArray<Tool.Any>): ReadonlyArray<string> =>\n definitions.flatMap((tool) => (tool.needsApproval === undefined || tool.needsApproval === false ? [] : [tool.name]))\n\n// With noUncheckedIndexedAccess, an index doesn't narrow on `hasTool`.\nconst toolNamed = (toolkit: { readonly tools: Record<string, Tool.Any> }, name: string): Tool.Any | undefined =>\n Object.hasOwn(toolkit.tools, name) ? toolkit.tools[name] : undefined\n\n// Params that don't fit the tool don't wait: the call runs, and fails with why.\nconst needsApprovalNative = Effect.fnUntraced(function* <T extends Record<string, Tool.Any>>(\n toolkit: { readonly tools: T },\n call: AssistantEvent.ToolCallPart,\n history: ReadonlyArray<Prompt.Message>\n): Effect.fn.Return<boolean, never, ToolServices<T>> {\n const tool = toolNamed(toolkit, call.name)\n const needs = tool?.needsApproval\n\n if (tool === undefined || needs === undefined || Predicate.isBoolean(needs)) return needs === true\n\n return yield* Effect.gen(function* () {\n const params = yield* Schema.decodeUnknownEffect(tool.parametersSchema)(call.params)\n const decided = needs(params, { toolCallId: call.id, messages: history })\n\n return Effect.isEffect(decided) ? yield* decided : decided\n }).pipe(Effect.orElseSucceed(() => false))\n})\n\n// The toolkit decodes the params with the tool's schema before its handler sees them, and a call whose params don't\n// fit fails with a validation error the model reads: the model's JSON is what `handle` takes.\nconst isParams = <T>(params: unknown): params is Tool.ParametersEncoded<T> => params !== undefined\n\n// Every failure is the call's result, for the model to read. With `failureMode: \"return\"` the toolkit encodes the\n// tool's own failure with its schema first.\n/** @internal */\nexport const runNative = Effect.fnUntraced(function* <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.WithHandler<T>,\n call: AssistantEvent.ToolCallPart\n): Effect.fn.Return<CallResult, never, ToolServices<T>> {\n const name: PropertyKey = call.name\n\n if (!hasTool(toolkit, name)) return failed(`No tool named ${call.name}.`, \"ToolNotFoundError\")\n\n const params: unknown = call.params\n\n if (!isParams<T[keyof T]>(params)) {\n return failed(`Invalid parameters for ${call.name}.`, \"ToolParameterValidationError\")\n }\n\n return yield* Effect.gen(function* () {\n const results = yield* toolkit.handle<keyof T>(name, params, call.id)\n const last = yield* Stream.runLast(Stream.filter(results, (result) => !result.preliminary))\n\n return Option.match(last, {\n onNone: () => failed(`${call.name} returned no result.`, \"NoResult\"),\n onSome: (result): CallResult =>\n result.isFailure\n ? failure(toJson(result.encodedResult))\n : { outcome: \"success\", result: toJson(result.encodedResult) }\n })\n }).pipe(\n Effect.catch((error) =>\n Effect.succeed(\n AiError.isAiError(error) ? failed(error.reason.message, error.reason._tag) : failure(toJson(error))\n )\n ),\n Effect.provideService(AssistantToolCall, { callId: call.id, tool: call.name })\n )\n})\n\n/** @internal */\nexport const fromWithHandler = <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.WithHandler<T>\n): Tools<ToolServices<T>> => ({\n definitions: Object.values(toolkit.tools),\n run: (call) => runNative(toolkit, call),\n needsApproval: (call, history) => needsApprovalNative(toolkit, call, history)\n})\n\n/** @internal */\nexport const fromToolkit = <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.Toolkit<T>\n): Tools<ToolServices<T> | Tool.HandlersFor<T>> => ({\n definitions: Object.values(toolkit.tools),\n run: Effect.fnUntraced(function* (call) {\n return yield* runNative(yield* toolkit, call)\n }),\n needsApproval: (call, history) => needsApprovalNative(toolkit, call, history)\n})\n\n/** @internal */\nexport const fromToolkitOrEmpty = <T extends Record<string, Tool.Any>>(\n toolkit: Toolkit.Toolkit<T> | undefined\n): Tools<ToolServices<T> | Tool.HandlersFor<T>> => (toolkit === undefined ? empty : fromToolkit(toolkit))\n\n/** @internal */\nexport const empty: Tools = {\n definitions: [],\n run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, \"ToolNotFoundError\")),\n needsApproval: () => Effect.succeed(false)\n}\n\n/** @internal */\nexport const merge = <A, B>(self: Tools<A>, that: Tools<B>): Tools<A | B> => {\n const inSelf = (call: AssistantEvent.ToolCallPart) => self.definitions.some((tool) => tool.name === call.name)\n\n return {\n definitions: [...self.definitions, ...that.definitions],\n run: (call) => (inSelf(call) ? self.run(call) : that.run(call)),\n needsApproval: (call, history) =>\n inSelf(call) ? self.needsApproval(call, history) : that.needsApproval(call, history)\n }\n}\n\n// A turn's own services, for a loop the app writes.\n/** @internal */\nexport const provide = <R>(tools: Tools<R>, context: Context.Context<R>): Tools => ({\n definitions: tools.definitions,\n run: (call) => Effect.provideContext(tools.run(call), context),\n needsApproval: (call, history) => Effect.provideContext(tools.needsApproval(call, history), context)\n})\n\n/** @internal */\nexport const duplicateNames = (tools: Tools<unknown>): ReadonlyArray<string> => {\n const names = tools.definitions.map((tool) => tool.name)\n\n return names.filter((name, i) => names.indexOf(name) !== i)\n}\n\n// The same tools are offered, so the prompt doesn't change.\n/** @internal */\nexport const runOnly = <R>(tools: Tools<R>, names: ReadonlyArray<string>, message: string): Tools<R> => ({\n definitions: tools.definitions,\n run: (call) => (names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, \"NotNow\"))),\n // Refused like any other call it doesn't run.\n needsApproval: () => Effect.succeed(false)\n})\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"turns.d.ts","names":[],"sources":["../../src/internal/turns.ts"],"mappings":";;;;;;;;;;;;;;;;;;qBAmCa;;qBAGA;;qBAIA;;iBAII;WACN;WACA,QAAQ;WACR,aAAa,cAAc;WAC3B,KAAK;;;YAKJ,eACV,MAAM,cACH,OAAO,OAAO,gBAA8B,eAAyB,cAAc;;iBAKvE;WACN,QAAQ;WACR,SAAS,OAAO,OAAO;WACvB,QAAQ;;;iBAIF;WACN,YAAY;WACZ,OAAO,QAAQ,QAAQ,cAAc;WACrC,OAAO,MAAM,cAAc,OAAO,OAAO,oBAAoB,MAAM,QAAQ;WAC3E,aAAa;aACX,YAAY;aACZ,OAAO;;WAET,UAAU,MAAe;;;qBAIvB,WAAQ,YAAA,YAAA;WAEO,MAAA;MAiC1B,OAAA,OAAA,qBAAA,cAAA;;qBA2MW,MAAG;WAEW,cAAA;WACL,SAAA;WACH,MAAA;WACI,UAAA,QAAQ,QAAQ,eAAyB,cAAc;WAEtD,WAAA,OAAO;MA4D9B,OAAA"}
1
+ {"version":3,"file":"turns.d.ts","names":[],"sources":["../../src/internal/turns.ts"],"mappings":";;;;;;;;;;;;;;;;;;qBAmCa;;qBAGA;;qBAIA;;iBAII;WACN;WACA,QAAQ;WACR,aAAa,cAAc;WAC3B,KAAK;;;YAKJ,eACV,MAAM,cACH,OAAO,OAAO,gBAA8B,eAAyB,cAAc;;iBAKvE;WACN,QAAQ;WACR,SAAS,OAAO,OAAO;WACvB,QAAQ;;;iBAIF;WACN,YAAY;WACZ,OAAO,QAAQ,QAAQ,cAAc;WACrC,OAAO,MAAM,cAAc,OAAO,OAAO,oBAAoB,MAAM,QAAQ;WAC3E,aAAa;aACX,YAAY;aACZ,OAAO;;WAET,UAAU,MAAe;;;qBAIvB,WAAQ,YAAA,YAAA;WAEO,MAAA;MAiC1B,OAAA,OAAA,qBAAA,cAAA;;qBA0NW,MAAG;WAEW,cAAA;WACL,SAAA;WACH,MAAA;WACI,UAAA,QAAQ,QAAQ,eAAyB,cAAc;WAEtD,WAAA,OAAO;MA8D9B,OAAA"}
@@ -1,16 +1,15 @@
1
1
  import { ToolResult, TurnCompleted, TurnFailed, TurnStarted, isLibraryEvent } from "../AssistantEvent.js";
2
+ import { fromEntries } from "../ToolApproval.js";
2
3
  import { decodeEntries } from "./events.js";
3
4
  import { toJsonText } from "./schema.js";
4
- import { fromEntries, mayHaveRunOutput, notRunOutput, owesDelivery, owesReply, unansweredCalls, unhandledInputs, unreadAfter, userText } from "./history.js";
5
+ import { fromEntries as fromEntries$1, mayHaveRunOutput, notRunOutput, owesDelivery, owesReply, unansweredCalls, unhandledInputs, unreadAfter, userText } from "./history.js";
5
6
  import { agentInput, agentOutput, agentSpan, currentTraceId } from "./telemetry.js";
6
7
  import { duplicateNames, fromToolkitOrEmpty, merge, provide, runOnly } from "./toolkit.js";
7
8
  import { makeMessaging, sendUserMessageName, waitToolName } from "./tools.js";
8
- import { TurnTools, run as run$1 } from "./loop.js";
9
- import { withCatalog } from "../Skill.js";
9
+ import { TurnApprovals, TurnTools, run as run$1 } from "./loop.js";
10
+ import { toolkit } from "./specialists.js";
10
11
  import { protocol } from "../Instructions.js";
11
- import { available, toTools } from "./skills.js";
12
12
  import { mapText } from "./instructions.js";
13
- import { toolkit } from "./specialists.js";
14
13
  import "effect/Context";
15
14
  import * as Effect from "effect/Effect";
16
15
  import * as Predicate from "effect/Predicate";
@@ -18,6 +17,7 @@ import * as Schema from "effect/Schema";
18
17
  import * as Stream from "effect/Stream";
19
18
  import * as Exit from "effect/Exit";
20
19
  import * as Option from "effect/Option";
20
+ import * as Response from "effect/ai/Response";
21
21
  import * as Ref from "effect/Ref";
22
22
  import * as Cause from "effect/Cause";
23
23
  import { identity } from "effect/Function";
@@ -56,11 +56,17 @@ const failureMessage = (cause) => Option.match(Schema.decodeUnknownOption(Provid
56
56
  onNone: () => Cause.pretty(cause),
57
57
  onSome: ({ _tag, raw }) => `${_tag ?? "Error"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`
58
58
  });
59
+ const approvedCalls = (entries) => [...fromEntries(entries).values()].flatMap(({ request, status, outcome }) => status === "approved" && outcome === void 0 ? [Response.makePart("tool-call", {
60
+ id: request.callId,
61
+ name: request.tool,
62
+ params: request.params,
63
+ providerExecuted: false
64
+ })] : []);
59
65
  const lastSeq = (entries, otherwise) => entries.at(-1)?.seq ?? otherwise;
60
66
  const closeOpenCalls = Effect.fnUntraced(function* (chat, startSeq) {
61
67
  const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq);
62
68
  const started = new Set(entries.flatMap(({ event }) => Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []));
63
- const open = unansweredCalls(fromEntries(entries));
69
+ const open = unansweredCalls(fromEntries$1(entries));
64
70
  if (open.length === 0) return;
65
71
  yield* chat.append(open.map((call) => ToolResult.make({
66
72
  callId: call.id,
@@ -100,7 +106,7 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
100
106
  yield* checkOpen;
101
107
  const all = yield* chat.entries;
102
108
  return {
103
- messages: fromEntries(view(decodeEntries(app, all))),
109
+ messages: fromEntries$1(view(decodeEntries(app, all))),
104
110
  throughSeq: lastSeq(all, startSeq)
105
111
  };
106
112
  });
@@ -120,12 +126,12 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
120
126
  yield* checkOpen;
121
127
  const entries = yield* chat.entries;
122
128
  return {
123
- messages: [...fromEntries(options.view(decodeEntries(app, entries))), userText(nudge)],
129
+ messages: [...fromEntries$1(options.view(decodeEntries(app, entries))), userText(nudge)],
124
130
  throughSeq: lastSeq(entries, startSeq)
125
131
  };
126
132
  }),
127
133
  maxSteps: 2
128
- });
134
+ }).pipe(Stream.provideService(TurnApprovals, Option.none()));
129
135
  }));
130
136
  const replyIfOwed = (options) => running(replyPass({
131
137
  instructions: instructions(options.instructions),
@@ -135,10 +141,9 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
135
141
  }));
136
142
  const prepare = (options) => {
137
143
  const own = fromToolkitOrEmpty(options.toolkit);
138
- const skills = available(options.skills ?? [], [...builtins.definitions, ...own.definitions]);
139
144
  return {
140
- system: mapText(instructions(options.instructions), (text) => withCatalog(text, skills)),
141
- toolkit: merge(merge(builtins, toTools(skills)), own),
145
+ system: instructions(options.instructions),
146
+ toolkit: merge(builtins, own),
142
147
  view: options.history ?? identity
143
148
  };
144
149
  };
@@ -183,7 +188,7 @@ const run = Effect.fnUntraced(function* (options) {
183
188
  const ranLoop = yield* Ref.make(false);
184
189
  const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop);
185
190
  const record = (event) => chat.append([event]);
186
- yield* options.handler(turn).pipe(Stream.runForEach(record), Effect.onExit((exit) => Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq)), Effect.provideService(TurnTools, Option.some(provide(options.registration.builtins, options.services))), Effect.provideContext(options.services), Effect.matchCauseEffect({
191
+ yield* options.handler(turn).pipe(Stream.runForEach(record), Effect.onExit((exit) => Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq)), Effect.provideService(TurnTools, Option.some(provide(options.registration.builtins, options.services))), Effect.provideService(TurnApprovals, Option.some({ approved: Effect.map(chat.entries, approvedCalls) })), Effect.provideContext(options.services), Effect.matchCauseEffect({
187
192
  onSuccess: Effect.fnUntraced(function* () {
188
193
  const all = yield* chat.entries;
189
194
  yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop));
@@ -1 +1 @@
1
- {"version":3,"file":"turns.js","names":["Toolkits.merge","Tools.makeMessaging","Specialists.toolkit","History.unansweredCalls","History.fromEntries","History.mayHaveRunOutput","History.notRunOutput","AssistantEvent.isLibraryEvent","History.owesReply","History.unreadAfter","AppEvents.decodeEntries","Instructions.protocol","History.owesDelivery","Loop.run","Toolkits.runOnly","Tools.sendUserMessageName","Tools.waitToolName","History.userText","Toolkits.fromToolkitOrEmpty","Skills.available","Skill.withCatalog","Skills.toTools","Toolkits.duplicateNames","Telemetry.agentInput","History.unhandledInputs","Telemetry.currentTraceId","Loop.TurnTools","Toolkits.provide","Telemetry.agentOutput","Telemetry.agentSpan"],"sources":["../../src/internal/turns.ts"],"sourcesContent":["import type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport { identity } from \"effect/Function\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type * as AssistantLoop from \"../AssistantLoop.ts\"\nimport * as Loop from \"./loop.ts\"\nimport type * as BlockKit from \"../BlockKit.ts\"\nimport type { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Instructions from \"../Instructions.ts\"\nimport * as Skill from \"../Skill.ts\"\nimport * as Skills from \"./skills.ts\"\nimport type * as Specialist from \"../Specialist.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport { mapText } from \"./instructions.ts\"\nimport { toJsonText } from \"./schema.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Telemetry from \"./telemetry.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport type { Writer } from \"./writer.ts\"\nimport * as Tools from \"./tools.ts\"\n\n/** @internal */\nexport const silentTurnMessage = \"I finished without sending a reply.\"\n\n/** @internal */\nexport const defaultReplyNudge =\n \"You haven't replied to the user's latest message yet. Reply now with send_user_message.\"\n\n/** @internal */\nexport const deliveryNudge =\n \"You told the user you're on it, but stopped without giving them the result. Send it now with send_user_message; if part of it is still running, say what and that you'll report back. If they already have everything, call wait.\"\n\n/** @internal */\nexport interface Definition {\n readonly name: string\n readonly blocks: BlockKit.BlockKit | undefined\n readonly specialists: ReadonlyArray<Specialist.Any>\n readonly app: AppEvents.AppEvents\n}\n\n// Once its build ran the layer's services are provided, so its streams need only what a turn provides.\n/** @internal */\nexport type TurnHandler = (\n turn: Assistant.Turn<any>\n) => Stream.Stream<AssistantLoop.Event, unknown, Assistant.TurnServices | LanguageModel.LanguageModel>\n\n// The chat `CurrentChat` gives, with its entries typed as the assistant's decode returns them: the assistant's typed\n// chat is this one, so it needs no check of its own.\n/** @internal */\nexport interface TypedChat {\n readonly chatId: ChatId\n readonly entries: Effect.Effect<AppEvents.DecodedEntries>\n readonly append: CurrentChat[\"Service\"][\"append\"]\n}\n\n/** @internal */\nexport interface Registration {\n readonly definition: Definition\n readonly model: Context.Context<LanguageModel.LanguageModel>\n readonly open: (chat: TypedChat) => Effect.Effect<TurnHandler, never, Scope.Scope | CurrentChat>\n readonly specialists: ReadonlyArray<{\n readonly specialist: Specialist.Any\n readonly built: Specialist.Built\n }>\n readonly builtins: Toolkits.Tools<Assistant.TurnServices>\n}\n\n/** @internal */\nexport const register = Effect.fnUntraced(function* (\n definition: Definition,\n options: { readonly open: Registration[\"open\"] }\n) {\n const model = yield* Effect.context<LanguageModel.LanguageModel>()\n\n const specialists = yield* Effect.forEach(\n definition.specialists,\n Effect.fnUntraced(function* (specialist) {\n const built = yield* Effect.serviceOption(specialist.Handler)\n\n if (Option.isNone(built)) {\n return yield* Effect.die(\n `No handler for the ${specialist.name} specialist: provide ${specialist.name}.toLayer.`\n )\n }\n\n return { specialist, built: built.value }\n })\n )\n\n const builtins = Toolkits.merge(\n yield* Tools.makeMessaging(definition.blocks),\n yield* Specialists.toolkit(definition.specialists)\n )\n\n const registration: Registration = {\n definition,\n model,\n open: options.open,\n specialists,\n builtins\n }\n\n return registration\n})\n\nconst ProviderError = Schema.Struct({ _tag: Schema.optional(Schema.String), raw: Schema.Unknown })\n\n// Provider errors keep their response body.\nconst failureMessage = (cause: Cause.Cause<unknown>): string =>\n Option.match(Schema.decodeUnknownOption(ProviderError)(Cause.squash(cause)), {\n onNone: () => Cause.pretty(cause),\n onSome: ({ _tag, raw }) => `${_tag ?? \"Error\"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`\n })\n\nconst lastSeq = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) => entries.at(-1)?.seq ?? otherwise\n\n// The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a\n// shutdown) leaves its calls open instead: the next turn runs them again.\nconst closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: number) {\n const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq)\n\n const started = new Set(\n entries.flatMap(({ event }) => (Predicate.isTagged(event, \"ToolStarted\") ? [event.callId] : []))\n )\n\n const open = History.unansweredCalls(History.fromEntries(entries))\n\n if (open.length === 0) return\n\n yield* chat.append(\n open.map((call) =>\n AssistantEvent.ToolResult.make({\n callId: call.id,\n tool: call.name,\n outcome: \"failure\",\n ...(started.has(call.id)\n ? { result: History.mayHaveRunOutput, reason: \"MayHaveRun\" }\n : { result: History.notRunOutput, reason: \"NotRun\" })\n })\n )\n )\n})\n\n// The inputs before the turn's `TurnStarted` (at `startSeq`) woke it; past that, what its model outputs record\n// reading.\nconst handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSeq: number) =>\n entries.reduce(\n (through, { seq, event }) =>\n seq > startSeq &&\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ModelOutput\") &&\n event.seenThroughSeq !== undefined\n ? Math.max(through, event.seenThroughSeq)\n : through,\n startSeq\n )\n\n// A turn that ran the loop fails visibly when the user's latest input got no reply; an input that arrived after what\n// its model read gets the next turn, which replies. A handler that ended without the loop decided that itself.\nconst finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranLoop: boolean) {\n const entries = yield* chat.entries\n\n const end =\n ranLoop && History.owesReply(entries, read) && !History.unreadAfter(entries, read)\n ? AssistantEvent.TurnFailed.make({ throughSeq: read, message: silentTurnMessage })\n : AssistantEvent.TurnCompleted.make({ throughSeq: read })\n\n yield* chat.append([end])\n})\n\n// Everything stored so far counts as read, so a failing input isn't retried forever.\nconst failTurn = Effect.fnUntraced(function* (chat: Writer, cause: Cause.Cause<unknown>) {\n yield* Effect.logError(\"assistant turn failed\", cause)\n\n const last = lastSeq(yield* chat.entries, 0)\n\n yield* chat.append([AssistantEvent.TurnFailed.make({ throughSeq: last, message: failureMessage(cause) })])\n})\n\nconst makeTurnApi = (\n registration: Registration,\n chat: Writer,\n startSeq: number,\n checkOpen: Effect.Effect<void>,\n ranLoop: Ref.Ref<boolean>\n): Assistant.Turn<any> => {\n const { definition, builtins } = registration\n const { app } = definition\n\n const running = <A, E, R>(stream: Stream.Stream<A, E, R>) =>\n Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)))\n\n // With how far into the chat it goes: the step records it on the model's output.\n // Read before every model call: a chat the app deleted meanwhile stops here.\n const history = Effect.fnUntraced(function* (view: Assistant.HistoryView<any> = identity) {\n yield* checkOpen\n\n const all = yield* chat.entries\n\n const read: AssistantLoop.ChatHistory = {\n messages: History.fromEntries(view(AppEvents.decodeEntries(app, all))),\n throughSeq: lastSeq(all, startSeq)\n }\n\n return read\n })\n\n const instructions = (given: string | Instructions.Resolved) =>\n mapText(given, (text) => `${Instructions.protocol}\\n\\n${text}`)\n\n // The loop's own prompt and tools, so the prompt stays cached, with the nudge at the end.\n const replyPass = <R>(options: {\n readonly instructions: Instructions.Resolved\n readonly toolkit: Toolkits.Tools<R>\n readonly view: Assistant.HistoryView<any>\n readonly nudge: string | undefined\n }) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const all = yield* chat.entries\n const through = handledThrough(all, startSeq)\n\n // Something arrived after what the loop's model read: the next turn reads it with the full loop, and replies.\n if (History.unreadAfter(all, through)) return Stream.empty\n\n const owed = History.owesReply(all, through)\n const undelivered = !owed && History.owesDelivery(all, through)\n\n if (!owed && !undelivered) return Stream.empty\n\n const nudge = undelivered ? deliveryNudge : (options.nudge ?? defaultReplyNudge)\n\n return Loop.run({\n instructions: options.instructions,\n // A reply that's owed can only be sent; a result it may have given may also be for nobody.\n toolkit: Toolkits.runOnly(\n options.toolkit,\n owed ? [Tools.sendUserMessageName] : [Tools.sendUserMessageName, Tools.waitToolName],\n \"Not now: reply to the user with send_user_message.\"\n ),\n history: Effect.gen(function* () {\n yield* checkOpen\n\n const entries = yield* chat.entries\n\n return {\n messages: [\n ...History.fromEntries(options.view(AppEvents.decodeEntries(app, entries))),\n History.userText(nudge)\n ],\n throughSeq: lastSeq(entries, startSeq)\n }\n }),\n // A second step, for a reply after a call it was refused.\n maxSteps: 2\n })\n })\n )\n\n const replyIfOwed = (options: Assistant.ReplyOptions) =>\n running(\n replyPass({\n instructions: instructions(options.instructions),\n toolkit: builtins,\n view: identity,\n nudge: options.nudge\n })\n )\n\n const prepare = <T extends Record<string, Tool.Any>, RS>(options: Assistant.LoopOptions<T, RS, any>) => {\n const own = Toolkits.fromToolkitOrEmpty(options.toolkit)\n const skills = Skills.available(options.skills ?? [], [...builtins.definitions, ...own.definitions])\n\n return {\n system: mapText(instructions(options.instructions), (text) => Skill.withCatalog(text, skills)),\n toolkit: Toolkits.merge(Toolkits.merge(builtins, Skills.toTools(skills)), own),\n view: options.history ?? identity\n }\n }\n\n return {\n chatId: chat.chatId,\n history,\n instructions,\n replyIfOwed,\n loop: (options) => {\n const { system, toolkit, view } = prepare(options)\n const twice = Toolkits.duplicateNames(toolkit)\n\n const loop =\n twice.length > 0\n ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(\", \")}`)\n : Loop.run({\n instructions: system,\n toolkit,\n history: history(view),\n maxSteps: options.maxSteps ?? 16\n })\n\n return running(Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge })))\n }\n }\n}\n\n// Every event the handler's loop emits is recorded before the loop is pulled again.\n/** @internal */\nexport const run = Effect.fnUntraced(\n function* (options: {\n readonly registration: Registration\n readonly handler: TurnHandler\n readonly chat: Writer\n readonly services: Context.Context<Assistant.TurnServices | LanguageModel.LanguageModel>\n // A chat the app deleted is deleted here, and the turn stops.\n readonly checkOpen: Effect.Effect<void>\n }) {\n const { chat } = options\n const before = yield* chat.entries\n\n yield* Telemetry.agentInput(History.unhandledInputs(before))\n\n const assistant = options.registration.definition.name\n const traceId = yield* Telemetry.currentTraceId\n\n const started = yield* chat.append([\n AssistantEvent.TurnStarted.make(\n Option.match(traceId, {\n onNone: () => ({ assistant }),\n onSome: (id) => ({ assistant, traceId: id })\n })\n )\n ])\n\n const startSeq = lastSeq(started, 0)\n const ranLoop = yield* Ref.make(false)\n const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop)\n\n const record = (event: AssistantLoop.Event) => chat.append([event])\n\n yield* options.handler(turn).pipe(\n Stream.runForEach(record),\n Effect.onExit((exit) => (Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq))),\n // A loop the app writes gets the assistant's own tools too, with this turn's services.\n Effect.provideService(\n Loop.TurnTools,\n Option.some(Toolkits.provide(options.registration.builtins, options.services))\n ),\n Effect.provideContext(options.services),\n Effect.matchCauseEffect({\n onSuccess: Effect.fnUntraced(function* () {\n const all = yield* chat.entries\n\n yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop))\n }),\n onFailure: (cause) => failTurn(chat, cause)\n })\n )\n\n // The trace's output is what the user saw this turn, and a failure if any.\n const shown = (yield* chat.entries).flatMap(({ event, seq }) =>\n seq > startSeq && (Predicate.isTagged(event, \"AssistantMessage\") || Predicate.isTagged(event, \"TurnFailed\"))\n ? [event]\n : []\n )\n\n yield* Telemetry.agentOutput(shown)\n },\n (effect, options) =>\n effect.pipe(\n Telemetry.agentSpan(options.registration.definition.name, {\n chatId: options.chat.chatId,\n root: true\n })\n )\n)\n"],"mappings":"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"}
1
+ {"version":3,"file":"turns.js","names":["Toolkits.merge","Tools.makeMessaging","Specialists.toolkit","ToolApproval.fromEntries","History.unansweredCalls","History.fromEntries","History.mayHaveRunOutput","History.notRunOutput","AssistantEvent.isLibraryEvent","History.owesReply","History.unreadAfter","AppEvents.decodeEntries","Instructions.protocol","History.owesDelivery","Loop.run","Toolkits.runOnly","Tools.sendUserMessageName","Tools.waitToolName","History.userText","Loop.TurnApprovals","Toolkits.fromToolkitOrEmpty","Toolkits.duplicateNames","Telemetry.agentInput","History.unhandledInputs","Telemetry.currentTraceId","Loop.TurnTools","Toolkits.provide","Telemetry.agentOutput","Telemetry.agentSpan"],"sources":["../../src/internal/turns.ts"],"sourcesContent":["import type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport * as Response from \"effect/ai/Response\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport { identity } from \"effect/Function\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type * as AssistantLoop from \"../AssistantLoop.ts\"\nimport * as Loop from \"./loop.ts\"\nimport type * as BlockKit from \"../BlockKit.ts\"\nimport type { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Instructions from \"../Instructions.ts\"\nimport * as ToolApproval from \"../ToolApproval.ts\"\nimport type * as Specialist from \"../Specialist.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport { mapText } from \"./instructions.ts\"\nimport { toJsonText } from \"./schema.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Telemetry from \"./telemetry.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport type { Writer } from \"./writer.ts\"\nimport * as Tools from \"./tools.ts\"\n\n/** @internal */\nexport const silentTurnMessage = \"I finished without sending a reply.\"\n\n/** @internal */\nexport const defaultReplyNudge =\n \"You haven't replied to the user's latest message yet. Reply now with send_user_message.\"\n\n/** @internal */\nexport const deliveryNudge =\n \"You told the user you're on it, but stopped without giving them the result. Send it now with send_user_message; if part of it is still running, say what and that you'll report back. If they already have everything, call wait.\"\n\n/** @internal */\nexport interface Definition {\n readonly name: string\n readonly blocks: BlockKit.BlockKit | undefined\n readonly specialists: ReadonlyArray<Specialist.Any>\n readonly app: AppEvents.AppEvents\n}\n\n// Once its build ran the layer's services are provided, so its streams need only what a turn provides.\n/** @internal */\nexport type TurnHandler = (\n turn: Assistant.Turn<any>\n) => Stream.Stream<AssistantLoop.Event, unknown, Assistant.TurnServices | LanguageModel.LanguageModel>\n\n// The chat `CurrentChat` gives, with its entries typed as the assistant's decode returns them: the assistant's typed\n// chat is this one, so it needs no check of its own.\n/** @internal */\nexport interface TypedChat {\n readonly chatId: ChatId\n readonly entries: Effect.Effect<AppEvents.DecodedEntries>\n readonly append: CurrentChat[\"Service\"][\"append\"]\n}\n\n/** @internal */\nexport interface Registration {\n readonly definition: Definition\n readonly model: Context.Context<LanguageModel.LanguageModel>\n readonly open: (chat: TypedChat) => Effect.Effect<TurnHandler, never, Scope.Scope | CurrentChat>\n readonly specialists: ReadonlyArray<{\n readonly specialist: Specialist.Any\n readonly built: Specialist.Built\n }>\n readonly builtins: Toolkits.Tools<Assistant.TurnServices>\n}\n\n/** @internal */\nexport const register = Effect.fnUntraced(function* (\n definition: Definition,\n options: { readonly open: Registration[\"open\"] }\n) {\n const model = yield* Effect.context<LanguageModel.LanguageModel>()\n\n const specialists = yield* Effect.forEach(\n definition.specialists,\n Effect.fnUntraced(function* (specialist) {\n const built = yield* Effect.serviceOption(specialist.Handler)\n\n if (Option.isNone(built)) {\n return yield* Effect.die(\n `No handler for the ${specialist.name} specialist: provide ${specialist.name}.toLayer.`\n )\n }\n\n return { specialist, built: built.value }\n })\n )\n\n const builtins = Toolkits.merge(\n yield* Tools.makeMessaging(definition.blocks),\n yield* Specialists.toolkit(definition.specialists)\n )\n\n const registration: Registration = {\n definition,\n model,\n open: options.open,\n specialists,\n builtins\n }\n\n return registration\n})\n\nconst ProviderError = Schema.Struct({ _tag: Schema.optional(Schema.String), raw: Schema.Unknown })\n\n// Provider errors keep their response body.\nconst failureMessage = (cause: Cause.Cause<unknown>): string =>\n Option.match(Schema.decodeUnknownOption(ProviderError)(Cause.squash(cause)), {\n onNone: () => Cause.pretty(cause),\n onSome: ({ _tag, raw }) => `${_tag ?? \"Error\"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`\n })\n\n// Approved, and not run yet: also one a crash cut off while it ran, which runs again with the same id.\nconst approvedCalls = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlyArray<AssistantEvent.ToolCallPart> =>\n [...ToolApproval.fromEntries(entries).values()].flatMap(({ request, status, outcome }) =>\n status === \"approved\" && outcome === undefined\n ? [\n Response.makePart(\"tool-call\", {\n id: request.callId,\n name: request.tool,\n params: request.params,\n providerExecuted: false\n })\n ]\n : []\n )\n\nconst lastSeq = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) => entries.at(-1)?.seq ?? otherwise\n\n// The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a\n// shutdown) leaves its calls open instead: the next turn runs them again.\nconst closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: number) {\n const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq)\n\n const started = new Set(\n entries.flatMap(({ event }) => (Predicate.isTagged(event, \"ToolStarted\") ? [event.callId] : []))\n )\n\n const open = History.unansweredCalls(History.fromEntries(entries))\n\n if (open.length === 0) return\n\n yield* chat.append(\n open.map((call) =>\n AssistantEvent.ToolResult.make({\n callId: call.id,\n tool: call.name,\n outcome: \"failure\",\n ...(started.has(call.id)\n ? { result: History.mayHaveRunOutput, reason: \"MayHaveRun\" }\n : { result: History.notRunOutput, reason: \"NotRun\" })\n })\n )\n )\n})\n\n// The inputs before the turn's `TurnStarted` (at `startSeq`) woke it; past that, what its model outputs record\n// reading.\nconst handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSeq: number) =>\n entries.reduce(\n (through, { seq, event }) =>\n seq > startSeq &&\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ModelOutput\") &&\n event.seenThroughSeq !== undefined\n ? Math.max(through, event.seenThroughSeq)\n : through,\n startSeq\n )\n\n// A turn that ran the loop fails visibly when the user's latest input got no reply; an input that arrived after what\n// its model read gets the next turn, which replies. A handler that ended without the loop decided that itself.\nconst finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranLoop: boolean) {\n const entries = yield* chat.entries\n\n const end =\n ranLoop && History.owesReply(entries, read) && !History.unreadAfter(entries, read)\n ? AssistantEvent.TurnFailed.make({ throughSeq: read, message: silentTurnMessage })\n : AssistantEvent.TurnCompleted.make({ throughSeq: read })\n\n yield* chat.append([end])\n})\n\n// Everything stored so far counts as read, so a failing input isn't retried forever.\nconst failTurn = Effect.fnUntraced(function* (chat: Writer, cause: Cause.Cause<unknown>) {\n yield* Effect.logError(\"assistant turn failed\", cause)\n\n const last = lastSeq(yield* chat.entries, 0)\n\n yield* chat.append([AssistantEvent.TurnFailed.make({ throughSeq: last, message: failureMessage(cause) })])\n})\n\nconst makeTurnApi = (\n registration: Registration,\n chat: Writer,\n startSeq: number,\n checkOpen: Effect.Effect<void>,\n ranLoop: Ref.Ref<boolean>\n): Assistant.Turn<any> => {\n const { definition, builtins } = registration\n const { app } = definition\n\n const running = <A, E, R>(stream: Stream.Stream<A, E, R>) =>\n Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)))\n\n // With how far into the chat it goes: the step records it on the model's output.\n // Read before every model call: a chat the app deleted meanwhile stops here.\n const history = Effect.fnUntraced(function* (view: Assistant.HistoryView<any> = identity) {\n yield* checkOpen\n\n const all = yield* chat.entries\n\n const read: AssistantLoop.ChatHistory = {\n messages: History.fromEntries(view(AppEvents.decodeEntries(app, all))),\n throughSeq: lastSeq(all, startSeq)\n }\n\n return read\n })\n\n const instructions = (given: string | Instructions.Resolved) =>\n mapText(given, (text) => `${Instructions.protocol}\\n\\n${text}`)\n\n // The loop's own prompt and tools, so the prompt stays cached, with the nudge at the end.\n const replyPass = <R>(options: {\n readonly instructions: Instructions.Resolved\n readonly toolkit: Toolkits.Tools<R>\n readonly view: Assistant.HistoryView<any>\n readonly nudge: string | undefined\n }) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const all = yield* chat.entries\n const through = handledThrough(all, startSeq)\n\n // Something arrived after what the loop's model read: the next turn reads it with the full loop, and replies.\n if (History.unreadAfter(all, through)) return Stream.empty\n\n const owed = History.owesReply(all, through)\n const undelivered = !owed && History.owesDelivery(all, through)\n\n if (!owed && !undelivered) return Stream.empty\n\n const nudge = undelivered ? deliveryNudge : (options.nudge ?? defaultReplyNudge)\n\n return Loop.run({\n instructions: options.instructions,\n // A reply that's owed can only be sent; a result it may have given may also be for nobody.\n toolkit: Toolkits.runOnly(\n options.toolkit,\n owed ? [Tools.sendUserMessageName] : [Tools.sendUserMessageName, Tools.waitToolName],\n \"Not now: reply to the user with send_user_message.\"\n ),\n history: Effect.gen(function* () {\n yield* checkOpen\n\n const entries = yield* chat.entries\n\n return {\n messages: [\n ...History.fromEntries(options.view(AppEvents.decodeEntries(app, entries))),\n History.userText(nudge)\n ],\n throughSeq: lastSeq(entries, startSeq)\n }\n }),\n // A second step, for a reply after a call it was refused.\n maxSteps: 2\n // The loop ran the approved calls; a pass that can only reply runs none.\n }).pipe(Stream.provideService(Loop.TurnApprovals, Option.none()))\n })\n )\n\n const replyIfOwed = (options: Assistant.ReplyOptions) =>\n running(\n replyPass({\n instructions: instructions(options.instructions),\n toolkit: builtins,\n view: identity,\n nudge: options.nudge\n })\n )\n\n const prepare = <T extends Record<string, Tool.Any>>(options: Assistant.LoopOptions<T, any>) => {\n const own = Toolkits.fromToolkitOrEmpty(options.toolkit)\n\n return {\n system: instructions(options.instructions),\n toolkit: Toolkits.merge(builtins, own),\n view: options.history ?? identity\n }\n }\n\n return {\n chatId: chat.chatId,\n history,\n instructions,\n replyIfOwed,\n loop: (options) => {\n const { system, toolkit, view } = prepare(options)\n const twice = Toolkits.duplicateNames(toolkit)\n\n const loop =\n twice.length > 0\n ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(\", \")}`)\n : Loop.run({\n instructions: system,\n toolkit,\n history: history(view),\n maxSteps: options.maxSteps ?? 16\n })\n\n return running(Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge })))\n }\n }\n}\n\n// Every event the handler's loop emits is recorded before the loop is pulled again.\n/** @internal */\nexport const run = Effect.fnUntraced(\n function* (options: {\n readonly registration: Registration\n readonly handler: TurnHandler\n readonly chat: Writer\n readonly services: Context.Context<Assistant.TurnServices | LanguageModel.LanguageModel>\n // A chat the app deleted is deleted here, and the turn stops.\n readonly checkOpen: Effect.Effect<void>\n }) {\n const { chat } = options\n const before = yield* chat.entries\n\n yield* Telemetry.agentInput(History.unhandledInputs(before))\n\n const assistant = options.registration.definition.name\n const traceId = yield* Telemetry.currentTraceId\n\n const started = yield* chat.append([\n AssistantEvent.TurnStarted.make(\n Option.match(traceId, {\n onNone: () => ({ assistant }),\n onSome: (id) => ({ assistant, traceId: id })\n })\n )\n ])\n\n const startSeq = lastSeq(started, 0)\n const ranLoop = yield* Ref.make(false)\n const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop)\n\n const record = (event: AssistantLoop.Event) => chat.append([event])\n\n yield* options.handler(turn).pipe(\n Stream.runForEach(record),\n Effect.onExit((exit) => (Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq))),\n // A loop the app writes gets the assistant's own tools too, with this turn's services.\n Effect.provideService(\n Loop.TurnTools,\n Option.some(Toolkits.provide(options.registration.builtins, options.services))\n ),\n // A turn's loop waits for the user's approval, and runs the calls they approved.\n Effect.provideService(Loop.TurnApprovals, Option.some({ approved: Effect.map(chat.entries, approvedCalls) })),\n Effect.provideContext(options.services),\n Effect.matchCauseEffect({\n onSuccess: Effect.fnUntraced(function* () {\n const all = yield* chat.entries\n\n yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop))\n }),\n onFailure: (cause) => failTurn(chat, cause)\n })\n )\n\n // The trace's output is what the user saw this turn, and a failure if any.\n const shown = (yield* chat.entries).flatMap(({ event, seq }) =>\n seq > startSeq && (Predicate.isTagged(event, \"AssistantMessage\") || Predicate.isTagged(event, \"TurnFailed\"))\n ? [event]\n : []\n )\n\n yield* Telemetry.agentOutput(shown)\n },\n (effect, options) =>\n effect.pipe(\n Telemetry.agentSpan(options.registration.definition.name, {\n chatId: options.chat.chatId,\n root: true\n })\n )\n)\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-assistant",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A colleague assistant on Effect: an append-only event log per chat, an interruptible agent loop, specialists, skills, and UI blocks",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/Assistant.ts CHANGED
@@ -34,7 +34,6 @@ import type { CurrentChat } from "./CurrentChat.ts"
34
34
  import type * as Instructions from "./Instructions.ts"
35
35
  import * as AppEvents from "./internal/events.ts"
36
36
  import * as Turns from "./internal/turns.ts"
37
- import type { Skill } from "./Skill.ts"
38
37
  import type * as Specialist from "./Specialist.ts"
39
38
 
40
39
  const TypeId = "~effect-assistant/Assistant" as const
@@ -54,21 +53,16 @@ export type TurnServices = CurrentChat | Specialist.Runner
54
53
  * @category models
55
54
  * @since 0.1.0
56
55
  */
57
- export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>, RS = never, Events = never> {
56
+ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>, Events = never> {
58
57
  /** The assistant's role and rules. The loop places `Instructions.protocol` before them. */
59
58
  readonly instructions: string | Instructions.Resolved
60
59
  /**
61
60
  * Tools the model can use in addition to the assistant's own. This is an `effect/ai` toolkit,
62
61
  * and its handlers come from the layer. Combine several toolkits with `Toolkit.merge`, for
63
- * example `Memory.Tools` or a `ToolDiscovery.make(...)` toolkit together with your own.
62
+ * example `Memory.Tools`, a `Skill.toolkit(...)`, or a `ToolDiscovery.make(...)` toolkit together
63
+ * with your own.
64
64
  */
65
65
  readonly toolkit?: Toolkit.Toolkit<T> | undefined
66
- /**
67
- * Playbooks the model can follow. The prompt lists them as a catalog, and the model loads one
68
- * with `load_skill` when a task matches it. A skill made with `tools` is left out when the turn's
69
- * tools lack one of them.
70
- */
71
- readonly skills?: ReadonlyArray<Skill<RS>> | undefined
72
66
  /** The maximum number of model calls per turn. Defaults to 16. */
73
67
  readonly maxSteps?: number | undefined
74
68
  /** The message the reply pass gives the model when the turn would end without a reply. */
@@ -188,15 +182,15 @@ export interface Turn<Events = never> {
188
182
  readonly instructions: (instructions: string | Instructions.Resolved) => Instructions.Resolved
189
183
  /**
190
184
  * Runs the agent loop over the chat. The loop uses `Instructions.protocol`, the assistant's own
191
- * tools, and the instructions, tools, and skills in `options`. Every step is a tool call, and
185
+ * tools, and the instructions and tools in `options`. Every step is a tool call, and
192
186
  * the loop ends with `replyIfOwed`.
193
187
  */
194
- readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>, RS = never>(
195
- options: LoopOptions<T, RS, Events>
188
+ readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(
189
+ options: LoopOptions<T, Events>
196
190
  ) => Stream.Stream<
197
191
  AssistantLoop.Event,
198
192
  AiError.AiError,
199
- LanguageModel.LanguageModel | TurnServices | AssistantLoop.ToolkitServices<T> | RS
193
+ LanguageModel.LanguageModel | TurnServices | AssistantLoop.ToolkitServices<T>
200
194
  >
201
195
  /**
202
196
  * Runs one more pass when the turn would otherwise end without a reply to the user's latest