effect-assistant 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Assistant.d.ts +5 -11
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantEvent.d.ts +66 -7
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +74 -9
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +4 -2
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +4 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.d.ts +63 -1
- package/dist/Block.d.ts.map +1 -1
- package/dist/Block.js +63 -2
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +25 -3
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +14 -12
- package/dist/BlockKit.js.map +1 -1
- package/dist/Compaction.d.ts +1 -1
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +11 -4
- package/dist/Compaction.js.map +1 -1
- package/dist/Skill.d.ts +85 -43
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +93 -44
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +6 -9
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +44 -6
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +87 -16
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +146 -0
- package/dist/ToolApproval.d.ts.map +1 -0
- package/dist/ToolApproval.js +201 -0
- package/dist/ToolApproval.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +6 -4
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolResult.d.ts +56 -0
- package/dist/ToolResult.d.ts.map +1 -0
- package/dist/ToolResult.js +49 -0
- package/dist/ToolResult.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -4
- package/dist/internal/chat.js +4 -1
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/history.js +28 -3
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +10 -5
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +60 -21
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +9 -16
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +11 -7
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +74 -21
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +39 -6
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -16
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +7 -13
- package/src/AssistantEvent.ts +73 -8
- package/src/AssistantLoop.ts +4 -2
- package/src/Block.ts +82 -1
- package/src/BlockKit.ts +37 -4
- package/src/Compaction.ts +19 -2
- package/src/Skill.ts +134 -57
- package/src/Specialist.ts +8 -11
- package/src/Timeline.ts +120 -21
- package/src/ToolApproval.ts +249 -0
- package/src/ToolDiscovery.ts +8 -3
- package/src/ToolResult.ts +80 -0
- package/src/index.ts +10 -0
- package/src/internal/chat.ts +11 -1
- package/src/internal/history.ts +58 -3
- package/src/internal/loop.ts +138 -35
- package/src/internal/specialists.ts +7 -13
- package/src/internal/toolkit.ts +102 -35
- package/src/internal/tools.ts +81 -5
- package/src/internal/turns.ts +31 -10
- package/dist/internal/skills.js +0 -33
- package/dist/internal/skills.js.map +0 -1
- package/src/internal/skills.ts +0 -48
|
@@ -6,11 +6,8 @@ import { Brief, Runner, SpecialistError } from "../Specialist.js";
|
|
|
6
6
|
import { toolMessage, userText } from "./history.js";
|
|
7
7
|
import { agentOutput, agentSpan } from "./telemetry.js";
|
|
8
8
|
import { ToolFailure, fail } from "../ToolFailure.js";
|
|
9
|
-
import { empty, fromToolkitOrEmpty, fromWithHandler
|
|
10
|
-
import { TurnTools, run } from "./loop.js";
|
|
11
|
-
import { withCatalog } from "../Skill.js";
|
|
12
|
-
import { available, toTools } from "./skills.js";
|
|
13
|
-
import { mapText } from "./instructions.js";
|
|
9
|
+
import { empty, fromToolkitOrEmpty, fromWithHandler } from "./toolkit.js";
|
|
10
|
+
import { TurnApprovals, TurnTools, run } from "./loop.js";
|
|
14
11
|
import * as Context from "effect/Context";
|
|
15
12
|
import * as Effect from "effect/Effect";
|
|
16
13
|
import * as Predicate from "effect/Predicate";
|
|
@@ -52,19 +49,15 @@ const runBrief = Effect.fnUntraced(function* (handler, options) {
|
|
|
52
49
|
chatId: options.chatId,
|
|
53
50
|
brief: options.brief,
|
|
54
51
|
history: current,
|
|
55
|
-
loop: (loop) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
history: current,
|
|
62
|
-
maxSteps: loop.maxSteps ?? 8
|
|
63
|
-
});
|
|
64
|
-
}
|
|
52
|
+
loop: (loop) => run({
|
|
53
|
+
instructions: loop.instructions,
|
|
54
|
+
toolkit: fromToolkitOrEmpty(loop.toolkit),
|
|
55
|
+
history: current,
|
|
56
|
+
maxSteps: loop.maxSteps ?? 8
|
|
57
|
+
}).pipe(Stream.provideService(TurnApprovals, Option.none()))
|
|
65
58
|
};
|
|
66
59
|
const record = Effect.fnUntraced(function* (event) {
|
|
67
|
-
if (Predicate.isTagged(event, "ToolStarted")) return;
|
|
60
|
+
if (Predicate.isTagged(event, "ToolStarted") || Predicate.isTagged(event, "ApprovalRequested")) return;
|
|
68
61
|
if (Predicate.isTagged(event, "ToolResult")) return yield* add([toolMessage(event)]);
|
|
69
62
|
yield* add(Prompt.fromResponseParts(event.parts).content);
|
|
70
63
|
const text = finalText(event);
|
|
@@ -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,31 +3,33 @@ 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,
|
|
10
|
+
export { CallResult, ToolServices, Tools, duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval, provide, resultIdOf, runNative, runOnly, showing };
|
|
10
11
|
}
|
|
11
12
|
/** @internal */
|
|
12
13
|
export interface CallResult {
|
|
13
14
|
readonly outcome: ToolOutcome;
|
|
14
15
|
readonly result: Schema.Json;
|
|
15
16
|
readonly reason?: string | undefined;
|
|
17
|
+
readonly full?: Schema.Json | undefined;
|
|
18
|
+
readonly resultId?: string | undefined;
|
|
16
19
|
}
|
|
17
20
|
/** @internal */
|
|
18
21
|
export interface Tools<out R = never> {
|
|
19
22
|
readonly definitions: ReadonlyArray<Tool.Any>;
|
|
20
23
|
readonly run: (call: ToolCallPart) => Effect.Effect<CallResult, never, R>;
|
|
24
|
+
readonly needsApproval: (call: ToolCallPart, history: ReadonlyArray<Prompt.Message>) => Effect.Effect<boolean, never, R>;
|
|
21
25
|
}
|
|
22
26
|
/** @internal */
|
|
23
|
-
export type ToolServices<T extends Record<string, Tool.Any>> = Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall
|
|
27
|
+
export type ToolServices<T extends Record<string, Tool.Any>> = Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall> | T[keyof T]["parametersSchema"]["DecodingServices"];
|
|
24
28
|
/** @internal */
|
|
25
29
|
export declare const failed: (message: string, reason: string) => CallResult;
|
|
26
30
|
/** @internal */
|
|
27
31
|
export declare const needingApproval: (definitions: ReadonlyArray<Tool.Any>) => ReadonlyArray<string>;
|
|
28
32
|
/** @internal */
|
|
29
|
-
export declare const needsApprovalMessage: (names: ReadonlyArray<string>) => string;
|
|
30
|
-
/** @internal */
|
|
31
33
|
export declare const runNative: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.WithHandler<T>, call: {
|
|
32
34
|
readonly type: "tool-call";
|
|
33
35
|
readonly id: string;
|
|
@@ -38,20 +40,22 @@ export declare const runNative: <T extends Record<string, Tool.Any>>(toolkit: To
|
|
|
38
40
|
readonly [x: string]: Schema.Json;
|
|
39
41
|
};
|
|
40
42
|
readonly name: string;
|
|
41
|
-
}) => Effect.Effect<CallResult, never,
|
|
43
|
+
}) => Effect.Effect<CallResult, never, ToolServices<T>>;
|
|
42
44
|
/** @internal */
|
|
43
45
|
export declare const fromWithHandler: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.WithHandler<T>) => Tools<ToolServices<T>>;
|
|
44
46
|
/** @internal */
|
|
45
47
|
export declare const fromToolkit: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>) => Tools<ToolServices<T> | Tool.HandlersFor<T>>;
|
|
46
48
|
/** @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
49
|
export declare const fromToolkitOrEmpty: <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T> | undefined) => Tools<ToolServices<T> | Tool.HandlersFor<T>>;
|
|
50
50
|
/** @internal */
|
|
51
51
|
export declare const empty: Tools;
|
|
52
52
|
/** @internal */
|
|
53
53
|
export declare const merge: <A, B>(self: Tools<A>, that: Tools<B>) => Tools<A | B>;
|
|
54
54
|
/** @internal */
|
|
55
|
+
export declare const resultIdOf: (callId: string) => string;
|
|
56
|
+
/** @internal */
|
|
57
|
+
export declare const showing: <R>(tools: Tools<R>, names: ReadonlySet<string>) => Tools<R>;
|
|
58
|
+
/** @internal */
|
|
55
59
|
export declare const provide: <R>(tools: Tools<R>, context: Context.Context<R>) => Tools;
|
|
56
60
|
/** @internal */
|
|
57
61
|
export declare const duplicateNames: (tools: Tools<unknown>) => ReadonlyArray<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolkit.d.ts","names":[],"sources":["../../src/internal/toolkit.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"toolkit.d.ts","names":[],"sources":["../../src/internal/toolkit.ts"],"mappings":";;;;;;;;;;;;iBAkBiB;WACN,SAAS;WACT,QAAQ,OAAO;WACf;WACA,OAAO,OAAO;WACd;;;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;;;;;;;;;;MA2CtF,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,aAAU;;qBAuBV,UAAW,GAAC,OAAS,MAAM,IAAE,OAAS,wBAAsB,MAAM;;qBAalE,UAAW,GAAC,OAAS,MAAM,IAAE,SAAW,QAAQ,QAAQ,OAAK;;qBAO7D,iBAAc,OAAW,mBAAiB;;qBAQ1C,UAAW,GAAC,OAAS,MAAM,IAAE,OAAS,uBAAqB,oBAAoB,MAAM"}
|
package/dist/internal/toolkit.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { fromKey } from "./uuid.js";
|
|
1
2
|
import { toJson } from "./schema.js";
|
|
2
3
|
import { AssistantToolCall } from "../AssistantToolCall.js";
|
|
3
4
|
import { ToolFailure } from "../ToolFailure.js";
|
|
5
|
+
import { ModelView } from "../ToolResult.js";
|
|
6
|
+
import * as Context from "effect/Context";
|
|
4
7
|
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
5
9
|
import * as Schema from "effect/Schema";
|
|
6
10
|
import * as Stream from "effect/Stream";
|
|
7
11
|
import * as Option from "effect/Option";
|
|
@@ -27,8 +31,20 @@ const failure = (result) => ({
|
|
|
27
31
|
const hasTool = (toolkit, name) => Object.hasOwn(toolkit.tools, name);
|
|
28
32
|
/** @internal */
|
|
29
33
|
const needingApproval = (definitions) => definitions.flatMap((tool) => tool.needsApproval === void 0 || tool.needsApproval === false ? [] : [tool.name]);
|
|
30
|
-
|
|
31
|
-
const
|
|
34
|
+
const toolNamed = (toolkit, name) => Object.hasOwn(toolkit.tools, name) ? toolkit.tools[name] : void 0;
|
|
35
|
+
const needsApprovalNative = Effect.fnUntraced(function* (toolkit, call, history) {
|
|
36
|
+
const tool = toolNamed(toolkit, call.name);
|
|
37
|
+
const needs = tool?.needsApproval;
|
|
38
|
+
if (tool === void 0 || needs === void 0 || Predicate.isBoolean(needs)) return needs === true;
|
|
39
|
+
return yield* Effect.gen(function* () {
|
|
40
|
+
const params = yield* Schema.decodeUnknownEffect(tool.parametersSchema)(call.params);
|
|
41
|
+
const decided = needs(params, {
|
|
42
|
+
toolCallId: call.id,
|
|
43
|
+
messages: history
|
|
44
|
+
});
|
|
45
|
+
return Effect.isEffect(decided) ? yield* decided : decided;
|
|
46
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
47
|
+
});
|
|
32
48
|
const isParams = (params) => params !== void 0;
|
|
33
49
|
/** @internal */
|
|
34
50
|
const runNative = Effect.fnUntraced(function* (toolkit, call) {
|
|
@@ -39,11 +55,23 @@ const runNative = Effect.fnUntraced(function* (toolkit, call) {
|
|
|
39
55
|
return yield* Effect.gen(function* () {
|
|
40
56
|
const results = yield* toolkit.handle(name, params, call.id);
|
|
41
57
|
const last = yield* Stream.runLast(Stream.filter(results, (result) => !result.preliminary));
|
|
58
|
+
const view = Option.flatMap(Option.fromUndefinedOr(toolNamed(toolkit, call.name)), (tool) => Context.getOption(tool.annotations, ModelView));
|
|
42
59
|
return Option.match(last, {
|
|
43
60
|
onNone: () => failed(`${call.name} returned no result.`, "NoResult"),
|
|
44
|
-
onSome: (result) =>
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
onSome: (result) => {
|
|
62
|
+
if (result.isFailure) return failure(toJson(result.encodedResult));
|
|
63
|
+
const full = toJson(result.encodedResult);
|
|
64
|
+
return Option.match(view, {
|
|
65
|
+
onNone: () => ({
|
|
66
|
+
outcome: "success",
|
|
67
|
+
result: full
|
|
68
|
+
}),
|
|
69
|
+
onSome: (read) => ({
|
|
70
|
+
outcome: "success",
|
|
71
|
+
result: toJson(read(result.result)),
|
|
72
|
+
full
|
|
73
|
+
})
|
|
74
|
+
});
|
|
47
75
|
}
|
|
48
76
|
});
|
|
49
77
|
}).pipe(Effect.catch((error) => Effect.succeed(AiError.isAiError(error) ? failed(error.reason.message, error.reason._tag) : failure(toJson(error)))), Effect.provideService(AssistantToolCall, {
|
|
@@ -54,43 +82,67 @@ const runNative = Effect.fnUntraced(function* (toolkit, call) {
|
|
|
54
82
|
/** @internal */
|
|
55
83
|
const fromWithHandler = (toolkit) => ({
|
|
56
84
|
definitions: Object.values(toolkit.tools),
|
|
57
|
-
run: (call) => runNative(toolkit, call)
|
|
85
|
+
run: (call) => runNative(toolkit, call),
|
|
86
|
+
needsApproval: (call, history) => needsApprovalNative(toolkit, call, history)
|
|
58
87
|
});
|
|
59
88
|
/** @internal */
|
|
60
89
|
const fromToolkit = (toolkit) => ({
|
|
61
90
|
definitions: Object.values(toolkit.tools),
|
|
62
91
|
run: Effect.fnUntraced(function* (call) {
|
|
63
92
|
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
|
-
})
|
|
93
|
+
}),
|
|
94
|
+
needsApproval: (call, history) => needsApprovalNative(toolkit, call, history)
|
|
74
95
|
});
|
|
75
96
|
/** @internal */
|
|
76
97
|
const fromToolkitOrEmpty = (toolkit) => toolkit === void 0 ? empty : fromToolkit(toolkit);
|
|
77
98
|
/** @internal */
|
|
78
99
|
const empty = {
|
|
79
100
|
definitions: [],
|
|
80
|
-
run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, "ToolNotFoundError"))
|
|
101
|
+
run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, "ToolNotFoundError")),
|
|
102
|
+
needsApproval: () => Effect.succeed(false)
|
|
81
103
|
};
|
|
82
104
|
/** @internal */
|
|
83
105
|
const merge = (self, that) => {
|
|
84
106
|
const inSelf = (call) => self.definitions.some((tool) => tool.name === call.name);
|
|
85
107
|
return {
|
|
86
108
|
definitions: [...self.definitions, ...that.definitions],
|
|
87
|
-
run: (call) => inSelf(call) ? self.run(call) : that.run(call)
|
|
109
|
+
run: (call) => inSelf(call) ? self.run(call) : that.run(call),
|
|
110
|
+
needsApproval: (call, history) => inSelf(call) ? self.needsApproval(call, history) : that.needsApproval(call, history)
|
|
88
111
|
};
|
|
89
112
|
};
|
|
90
113
|
/** @internal */
|
|
114
|
+
const resultIdOf = (callId) => `r-${fromKey(`result:${callId}`).slice(0, 8)}`;
|
|
115
|
+
const decodeJsonObject = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Json));
|
|
116
|
+
const withResultId = (result, resultId) => {
|
|
117
|
+
if (result.outcome === "failure") return result;
|
|
118
|
+
const read = result.result;
|
|
119
|
+
const fields = Array.isArray(read) ? Option.none() : decodeJsonObject(read);
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
result: Option.match(fields, {
|
|
123
|
+
onNone: () => ({
|
|
124
|
+
resultId,
|
|
125
|
+
result: read
|
|
126
|
+
}),
|
|
127
|
+
onSome: (object) => ({
|
|
128
|
+
resultId,
|
|
129
|
+
...object
|
|
130
|
+
})
|
|
131
|
+
}),
|
|
132
|
+
full: result.full ?? read,
|
|
133
|
+
resultId
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** @internal */
|
|
137
|
+
const showing = (tools, names) => names.size === 0 ? tools : {
|
|
138
|
+
...tools,
|
|
139
|
+
run: (call) => names.has(call.name) ? Effect.map(tools.run(call), (result) => withResultId(result, resultIdOf(call.id))) : tools.run(call)
|
|
140
|
+
};
|
|
141
|
+
/** @internal */
|
|
91
142
|
const provide = (tools, context) => ({
|
|
92
143
|
definitions: tools.definitions,
|
|
93
|
-
run: (call) => Effect.provideContext(tools.run(call), context)
|
|
144
|
+
run: (call) => Effect.provideContext(tools.run(call), context),
|
|
145
|
+
needsApproval: (call, history) => Effect.provideContext(tools.needsApproval(call, history), context)
|
|
94
146
|
});
|
|
95
147
|
/** @internal */
|
|
96
148
|
const duplicateNames = (tools) => {
|
|
@@ -100,9 +152,10 @@ const duplicateNames = (tools) => {
|
|
|
100
152
|
/** @internal */
|
|
101
153
|
const runOnly = (tools, names, message) => ({
|
|
102
154
|
definitions: tools.definitions,
|
|
103
|
-
run: (call) => names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow"))
|
|
155
|
+
run: (call) => names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow")),
|
|
156
|
+
needsApproval: () => Effect.succeed(false)
|
|
104
157
|
});
|
|
105
158
|
//#endregion
|
|
106
|
-
export { duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval,
|
|
159
|
+
export { duplicateNames, empty, failed, fromToolkit, fromToolkitOrEmpty, fromWithHandler, merge, needingApproval, provide, resultIdOf, runNative, runOnly, showing };
|
|
107
160
|
|
|
108
161
|
//# 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":"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"}
|
|
1
|
+
{"version":3,"file":"toolkit.js","names":["ToolFailure.ToolFailure","Uuid.fromKey"],"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 * 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 { ModelView } from \"../ToolResult.ts\"\nimport { toJson } from \"./schema.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport interface CallResult {\n readonly outcome: AssistantEvent.ToolOutcome\n readonly result: Schema.Json\n readonly reason?: string | undefined\n readonly full?: Schema.Json | undefined\n readonly resultId?: 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 const view = Option.flatMap(Option.fromUndefinedOr(toolNamed(toolkit, call.name)), (tool) =>\n Context.getOption(tool.annotations, ModelView)\n )\n\n return Option.match(last, {\n onNone: () => failed(`${call.name} returned no result.`, \"NoResult\"),\n onSome: (result): CallResult => {\n if (result.isFailure) return failure(toJson(result.encodedResult))\n\n const full = toJson(result.encodedResult)\n\n return Option.match(view, {\n onNone: () => ({ outcome: \"success\", result: full }),\n onSome: (read) => ({ outcome: \"success\", result: toJson(read(result.result)), full })\n })\n }\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// Short, for the model to copy, and from the call: a call run again after a crash keeps its id.\n/** @internal */\nexport const resultIdOf = (callId: string): string => `r-${Uuid.fromKey(`result:${callId}`).slice(0, 8)}`\n\nconst decodeJsonObject = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Json))\n\nconst withResultId = (result: CallResult, resultId: string): CallResult => {\n if (result.outcome === \"failure\") return result\n\n const read = result.result\n const fields = Array.isArray(read) ? Option.none() : decodeJsonObject(read)\n\n return {\n ...result,\n result: Option.match(fields, {\n onNone: () => ({ resultId, result: read }),\n onSome: (object) => ({ resultId, ...object })\n }),\n full: result.full ?? read,\n resultId\n }\n}\n\n// The results of tools the assistant's blocks show get an id the model passes to show one.\n/** @internal */\nexport const showing = <R>(tools: Tools<R>, names: ReadonlySet<string>): Tools<R> =>\n names.size === 0\n ? tools\n : {\n ...tools,\n run: (call) =>\n names.has(call.name)\n ? Effect.map(tools.run(call), (result) => withResultId(result, resultIdOf(call.id)))\n : tools.run(call)\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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"}
|
package/dist/internal/tools.js
CHANGED
|
@@ -4,6 +4,7 @@ import { CurrentChat } from "../CurrentChat.js";
|
|
|
4
4
|
import { AssistantToolCall } from "../AssistantToolCall.js";
|
|
5
5
|
import { ToolFailure, fail } from "../ToolFailure.js";
|
|
6
6
|
import { fromWithHandler } from "./toolkit.js";
|
|
7
|
+
import { isToolResult } from "../Block.js";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
8
9
|
import * as Predicate from "effect/Predicate";
|
|
9
10
|
import * as Schema from "effect/Schema";
|
|
@@ -22,6 +23,9 @@ const Wait = Tool.make(waitToolName, {
|
|
|
22
23
|
const description = "Send the user a chat message. It's the only way they see anything you write: text outside it is private to you.";
|
|
23
24
|
const WaitFlag = Schema.Boolean.annotate({ description: "true when this is your last message for now: your turn ends with it, as with the wait tool. false when you keep working after it." });
|
|
24
25
|
const endsWithMessage = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(true) }));
|
|
26
|
+
const messageText = Schema.decodeUnknownOption(Schema.Struct({ text: Schema.String }));
|
|
27
|
+
/** @internal */
|
|
28
|
+
const isUserMessage = (call) => call.name === "send_user_message" && Option.exists(messageText(call.params), ({ text }) => text.trim() !== "");
|
|
25
29
|
/** @internal */
|
|
26
30
|
const isTurnEnding = (call) => call.name === "wait" || call.name === "send_user_message" && Option.isSome(endsWithMessage(call.params));
|
|
27
31
|
const messageId = AssistantToolCall.useSync((call) => fromKey(`message:${call.callId}`));
|
|
@@ -53,9 +57,22 @@ const plainHandlers = PlainTools.toHandlers({
|
|
|
53
57
|
const plainTools = Effect.gen(function* () {
|
|
54
58
|
return yield* Effect.provideContext(PlainTools, yield* plainHandlers);
|
|
55
59
|
});
|
|
60
|
+
/** @internal */
|
|
61
|
+
const toolResultKind = "tool_result";
|
|
62
|
+
const ToolResultRef = Schema.Struct({ resultId: Schema.String.annotate({ description: "The resultId of the tool result to show." }) });
|
|
63
|
+
const decodeRef = Schema.decodeUnknownOption(ToolResultRef);
|
|
64
|
+
const shownResult = Effect.fnUntraced(function* (resultId, shown) {
|
|
65
|
+
const result = (yield* (yield* CurrentChat).entries).flatMap(({ event }) => isLibraryEvent(event) && Predicate.isTagged(event, "ToolResult") && event.resultId === resultId ? [event] : []).at(-1);
|
|
66
|
+
if (result === void 0 || result.outcome === "failure") return yield* fail(`No tool result ${resultId} to show: pass the resultId of a result you got in this chat.`, { reason: "ResultNotFound" });
|
|
67
|
+
if (!shown.has(result.tool)) return yield* fail(`${result.tool} results can't be shown.`, { reason: "ResultNotShown" });
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
56
70
|
const blockTools = Effect.fnUntraced(function* (kit) {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
71
|
+
const asked = kit.blocks.filter((block) => !isToolResult(block));
|
|
72
|
+
const shown = new Set(kit.blocks.flatMap((block) => isToolResult(block) ? [block.tool.name] : []));
|
|
73
|
+
const catalog = [...asked.map((block) => `- ${block.kind}: ${block.description}`), ...shown.size === 0 ? [] : [`- ${toolResultKind}: a tool's result, shown the way the app draws it (found items as cards, a download as a button). Results you can show have a resultId: pass it as the props. From: ${[...shown].join(", ")}.`]].join("\n");
|
|
74
|
+
const kinds = [...asked.map((block) => block.kind), ...shown.size === 0 ? [] : [toolResultKind]];
|
|
75
|
+
const props = [...asked.map((block) => block.props), ...shown.size === 0 ? [] : [ToolResultRef]];
|
|
59
76
|
const BlockMessage = Tool.make(sendUserMessageName, {
|
|
60
77
|
description: `${description}
|
|
61
78
|
You can add a block to show UI with the message, e.g. a few options to pick from or a yes/no, when that makes answering easier than typing. Plain text is fine too. Using it sends you an <answer>; the user may also just type their answer instead, and then you pass that message's id in closes.
|
|
@@ -66,7 +83,7 @@ ${catalog}`,
|
|
|
66
83
|
wait: WaitFlag,
|
|
67
84
|
block: Schema.optional(Schema.Struct({
|
|
68
85
|
kind: Schema.Literals(kinds),
|
|
69
|
-
props: Schema.Union(
|
|
86
|
+
props: Schema.Union(props)
|
|
70
87
|
})),
|
|
71
88
|
closes: Schema.optional(Schema.Array(Schema.String)).annotate({ description: "Ids of your earlier messages whose block the user just answered by typing instead of using it. Those blocks close. Leave it out otherwise: a block stays open when the user talks about something else." })
|
|
72
89
|
}),
|
|
@@ -89,8 +106,24 @@ ${catalog}`,
|
|
|
89
106
|
}));
|
|
90
107
|
return "sent";
|
|
91
108
|
}
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
if (block.kind === "tool_result") {
|
|
110
|
+
const ref = decodeRef(block.props);
|
|
111
|
+
if (Option.isNone(ref)) return yield* fail(`A ${toolResultKind} block's props are { resultId }.`);
|
|
112
|
+
const result = yield* shownResult(ref.value.resultId, shown);
|
|
113
|
+
yield* append(AssistantMessage.make({
|
|
114
|
+
id,
|
|
115
|
+
text,
|
|
116
|
+
block: MessageBlock.make({
|
|
117
|
+
kind: result.tool,
|
|
118
|
+
props: {},
|
|
119
|
+
resultId: ref.value.resultId
|
|
120
|
+
}),
|
|
121
|
+
...closed
|
|
122
|
+
}));
|
|
123
|
+
return `sent as message ${id}`;
|
|
124
|
+
}
|
|
125
|
+
const schema = kit.byKind.get(block.kind)?.props;
|
|
126
|
+
if (schema === void 0 || Option.isNone(Schema.decodeUnknownOption(schema)(block.props))) return yield* fail(`Those props don't fit a ${block.kind} block.`);
|
|
94
127
|
yield* append(AssistantMessage.make({
|
|
95
128
|
id,
|
|
96
129
|
text,
|
|
@@ -108,6 +141,6 @@ const makeMessaging = Effect.fnUntraced(function* (kit) {
|
|
|
108
141
|
return fromWithHandler(yield* blockTools(kit));
|
|
109
142
|
});
|
|
110
143
|
//#endregion
|
|
111
|
-
export { isTurnEnding, makeMessaging, sendUserMessageName, waitToolName };
|
|
144
|
+
export { isTurnEnding, isUserMessage, makeMessaging, sendUserMessageName, toolResultKind, waitToolName };
|
|
112
145
|
|
|
113
146
|
//# sourceMappingURL=tools.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","names":["Uuid.fromKey","AssistantEvent.isLibraryEvent","ToolFailure.ToolFailure","ToolFailure.fail","Toolkits.fromWithHandler"],"sources":["../../src/internal/tools.ts"],"sourcesContent":["import * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\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 AssistantEvent from \"../AssistantEvent.ts\"\nimport { AssistantToolCall } from \"../AssistantToolCall.ts\"\nimport type * as BlockKit from \"../BlockKit.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport const waitToolName = \"wait\"\n\n/** @internal */\nexport const sendUserMessageName = \"send_user_message\"\n\nconst Wait = Tool.make(waitToolName, {\n description:\n \"End your turn and wait for the next message or update, when you have nothing more to do right now and no message to send (otherwise set wait on send_user_message).\",\n success: Schema.String\n})\n\nconst description =\n \"Send the user a chat message. It's the only way they see anything you write: text outside it is private to you.\"\n\nconst WaitFlag = Schema.Boolean.annotate({\n description:\n \"true when this is your last message for now: your turn ends with it, as with the wait tool. false when you keep working after it.\"\n})\n\nconst endsWithMessage = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(true) }))\n\n/** @internal */\nexport const isTurnEnding = (call: AssistantEvent.ToolCallPart): boolean =>\n call.name === waitToolName || (call.name === sendUserMessageName && Option.isSome(endsWithMessage(call.params)))\n\n// From the call: a call cut off by a crash runs again with the same id, and finds its message already sent.\nconst messageId = AssistantToolCall.useSync((call) => Uuid.fromKey(`message:${call.callId}`))\n\nconst append = Effect.fnUntraced(function* (message: AssistantEvent.AssistantMessage) {\n const chat = yield* CurrentChat\n const entries = yield* chat.entries\n\n const sent = entries.some(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"AssistantMessage\") && event.id === message.id\n )\n\n if (!sent) yield* chat.append(message)\n})\n\nconst PlainMessage = Tool.make(sendUserMessageName, {\n description,\n parameters: Schema.Struct({ text: Schema.String, wait: WaitFlag }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, AssistantToolCall]\n})\n\nconst PlainTools = Toolkit.make(PlainMessage, Wait)\n\nconst plainHandlers = PlainTools.toHandlers({\n wait: () => Effect.succeed(\"waiting\"),\n send_user_message: Effect.fnUntraced(function* ({ text }) {\n yield* append(AssistantEvent.AssistantMessage.make({ id: yield* messageId, text }))\n\n return \"sent\"\n })\n})\n\nconst plainTools = Effect.gen(function* () {\n return yield* Effect.provideContext(PlainTools, yield* plainHandlers)\n})\n\nconst blockTools = Effect.fnUntraced(function* (kit: BlockKit.BlockKit) {\n const catalog = kit.blocks.map((block) => `- ${block.kind}: ${block.description}`).join(\"\\n\")\n const kinds = kit.blocks.map((block) => block.kind)\n\n const BlockMessage = Tool.make(sendUserMessageName, {\n description: `${description}\nYou can add a block to show UI with the message, e.g. a few options to pick from or a yes/no, when that makes answering easier than typing. Plain text is fine too. Using it sends you an <answer>; the user may also just type their answer instead, and then you pass that message's id in closes.\nBlocks:\n${catalog}`,\n // Props are checked against their kind's schema by the handler; the model sees every kind's in the parameters.\n parameters: Schema.Struct({\n text: Schema.String,\n wait: WaitFlag,\n block: Schema.optional(\n Schema.Struct({\n kind: Schema.Literals(kinds),\n props: Schema.Union(kit.blocks.map((block) => block.props))\n })\n ),\n closes: Schema.optional(Schema.Array(Schema.String)).annotate({\n description:\n \"Ids of your earlier messages whose block the user just answered by typing instead of using it. Those blocks close. Leave it out otherwise: a block stays open when the user talks about something else.\"\n })\n }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, AssistantToolCall]\n })\n\n const BlockTools = Toolkit.make(BlockMessage, Wait)\n\n const handlers = yield* BlockTools.toHandlers({\n wait: () => Effect.succeed(\"waiting\"),\n send_user_message: Effect.fnUntraced(function* ({ text, block, closes }) {\n if (text.trim() === \"\") {\n return yield* ToolFailure.fail(\n \"The message has no text: the user sees only what you send here, and a block alone is an empty bubble. Send it again with the words they should read (a short lead-in for the block).\",\n { reason: \"EmptyMessage\" }\n )\n }\n\n const id = yield* messageId\n const closed = closes === undefined || closes.length === 0 ? {} : { closes }\n\n if (block === undefined) {\n yield* append(AssistantEvent.AssistantMessage.make({ id, text, ...closed }))\n\n return \"sent\"\n }\n\n const props = kit.byKind.get(block.kind)?.props\n\n if (props === undefined || Option.isNone(Schema.decodeUnknownOption(props)(block.props))) {\n return yield* ToolFailure.fail(`Those props don't fit a ${block.kind} block.`)\n }\n\n yield* append(\n AssistantEvent.AssistantMessage.make({\n id,\n text,\n block: AssistantEvent.MessageBlock.make(block),\n ...closed\n })\n )\n\n return `sent as message ${id}`\n })\n })\n\n return yield* Effect.provideContext(BlockTools, handlers)\n})\n\n/** @internal */\nexport const makeMessaging: (kit: BlockKit.BlockKit | undefined) => Effect.Effect<Toolkits.Tools<CurrentChat>> =\n Effect.fnUntraced(function* (kit) {\n if (kit === undefined || kit.blocks.length === 0) {\n return Toolkits.fromWithHandler(yield* plainTools)\n }\n\n return Toolkits.fromWithHandler(yield* blockTools(kit))\n })\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAa,eAAe;;AAG5B,MAAa,sBAAsB;AAEnC,MAAM,OAAO,KAAK,KAAK,cAAc;CACnC,aACE;CACF,SAAS,OAAO;AAClB,CAAC;AAED,MAAM,cACJ;AAEF,MAAM,WAAW,OAAO,QAAQ,SAAS,EACvC,aACE,oIACJ,CAAC;AAED,MAAM,kBAAkB,OAAO,oBAAoB,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,IAAI,EAAE,CAAC,CAAC;;AAGhG,MAAa,gBAAgB,SAC3B,KAAK,SAAA,UAA0B,KAAK,SAAA,uBAAgC,OAAO,OAAO,gBAAgB,KAAK,MAAM,CAAC;AAGhH,MAAM,YAAY,kBAAkB,SAAS,SAASA,QAAa,WAAW,KAAK,QAAQ,CAAC;AAE5F,MAAM,SAAS,OAAO,WAAW,WAAW,SAA0C;CACpF,MAAM,OAAO,OAAO;CAQpB,IAAI,EALS,OAFU,KAAK,QAAA,CAEP,MAClB,EAAE,YACDC,eAA8B,KAAK,KAAK,UAAU,SAAS,OAAO,kBAAkB,KAAK,MAAM,OAAO,QAAQ,EAG1G,GAAG,OAAO,KAAK,OAAO,OAAO;AACvC,CAAC;AAED,MAAM,eAAe,KAAK,KAAK,qBAAqB;CAClD;CACA,YAAY,OAAO,OAAO;EAAE,MAAM,OAAO;EAAQ,MAAM;CAAS,CAAC;CACjE,SAAS,OAAO;CAChB,SAASC;CACT,cAAc,CAAC,aAAa,iBAAiB;AAC/C,CAAC;AAED,MAAM,aAAa,QAAQ,KAAK,cAAc,IAAI;AAElD,MAAM,gBAAgB,WAAW,WAAW;CAC1C,YAAY,OAAO,QAAQ,SAAS;CACpC,mBAAmB,OAAO,WAAW,WAAW,EAAE,QAAQ;EACxD,OAAO,OAAA,iBAAuC,KAAK;GAAE,IAAI,OAAO;GAAW;EAAK,CAAC,CAAC;EAElF,OAAO;CACT,CAAC;AACH,CAAC;AAED,MAAM,aAAa,OAAO,IAAI,aAAa;CACzC,OAAO,OAAO,OAAO,eAAe,YAAY,OAAO,aAAa;AACtE,CAAC;AAED,MAAM,aAAa,OAAO,WAAW,WAAW,KAAwB;CACtE,MAAM,UAAU,IAAI,OAAO,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa,CAAC,CAAC,KAAK,IAAI;CAC5F,MAAM,QAAQ,IAAI,OAAO,KAAK,UAAU,MAAM,IAAI;CAElD,MAAM,eAAe,KAAK,KAAK,qBAAqB;EAClD,aAAa,GAAG,YAAY;;;EAG9B;EAEE,YAAY,OAAO,OAAO;GACxB,MAAM,OAAO;GACb,MAAM;GACN,OAAO,OAAO,SACZ,OAAO,OAAO;IACZ,MAAM,OAAO,SAAS,KAAK;IAC3B,OAAO,OAAO,MAAM,IAAI,OAAO,KAAK,UAAU,MAAM,KAAK,CAAC;GAC5D,CAAC,CACH;GACA,QAAQ,OAAO,SAAS,OAAO,MAAM,OAAO,MAAM,CAAC,CAAC,CAAC,SAAS,EAC5D,aACE,0MACJ,CAAC;EACH,CAAC;EACD,SAAS,OAAO;EAChB,SAASA;EACT,cAAc,CAAC,aAAa,iBAAiB;CAC/C,CAAC;CAED,MAAM,aAAa,QAAQ,KAAK,cAAc,IAAI;CAElD,MAAM,WAAW,OAAO,WAAW,WAAW;EAC5C,YAAY,OAAO,QAAQ,SAAS;EACpC,mBAAmB,OAAO,WAAW,WAAW,EAAE,MAAM,OAAO,UAAU;GACvE,IAAI,KAAK,KAAK,MAAM,IAClB,OAAO,OAAOC,KACZ,wLACA,EAAE,QAAQ,eAAe,CAC3B;GAGF,MAAM,KAAK,OAAO;GAClB,MAAM,SAAS,WAAW,KAAA,KAAa,OAAO,WAAW,IAAI,CAAC,IAAI,EAAE,OAAO;GAE3E,IAAI,UAAU,KAAA,GAAW;IACvB,OAAO,OAAA,iBAAuC,KAAK;KAAE;KAAI;KAAM,GAAG;IAAO,CAAC,CAAC;IAE3E,OAAO;GACT;GAEA,MAAM,QAAQ,IAAI,OAAO,IAAI,MAAM,IAAI,CAAC,EAAE;GAE1C,IAAI,UAAU,KAAA,KAAa,OAAO,OAAO,OAAO,oBAAoB,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,GACrF,OAAO,OAAOA,KAAiB,2BAA2B,MAAM,KAAK,QAAQ;GAG/E,OAAO,OAAA,iBAC2B,KAAK;IACnC;IACA;IACA,OAAA,aAAmC,KAAK,KAAK;IAC7C,GAAG;GACL,CAAC,CACH;GAEA,OAAO,mBAAmB;EAC5B,CAAC;CACH,CAAC;CAED,OAAO,OAAO,OAAO,eAAe,YAAY,QAAQ;AAC1D,CAAC;;AAGD,MAAa,gBACX,OAAO,WAAW,WAAW,KAAK;CAChC,IAAI,QAAQ,KAAA,KAAa,IAAI,OAAO,WAAW,GAC7C,OAAOC,gBAAyB,OAAO,UAAU;CAGnD,OAAOA,gBAAyB,OAAO,WAAW,GAAG,CAAC;AACxD,CAAC"}
|
|
1
|
+
{"version":3,"file":"tools.js","names":["Uuid.fromKey","AssistantEvent.isLibraryEvent","ToolFailure.ToolFailure","ToolFailure.fail","Block.isToolResult","Toolkits.fromWithHandler"],"sources":["../../src/internal/tools.ts"],"sourcesContent":["import * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\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 AssistantEvent from \"../AssistantEvent.ts\"\nimport { AssistantToolCall } from \"../AssistantToolCall.ts\"\nimport * as Block from \"../Block.ts\"\nimport type * as BlockKit from \"../BlockKit.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as ToolFailure from \"../ToolFailure.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport const waitToolName = \"wait\"\n\n/** @internal */\nexport const sendUserMessageName = \"send_user_message\"\n\nconst Wait = Tool.make(waitToolName, {\n description:\n \"End your turn and wait for the next message or update, when you have nothing more to do right now and no message to send (otherwise set wait on send_user_message).\",\n success: Schema.String\n})\n\nconst description =\n \"Send the user a chat message. It's the only way they see anything you write: text outside it is private to you.\"\n\nconst WaitFlag = Schema.Boolean.annotate({\n description:\n \"true when this is your last message for now: your turn ends with it, as with the wait tool. false when you keep working after it.\"\n})\n\nconst endsWithMessage = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(true) }))\n\nconst messageText = Schema.decodeUnknownOption(Schema.Struct({ text: Schema.String }))\n\n// A message the user reads: the call's text isn't empty.\n/** @internal */\nexport const isUserMessage = (call: AssistantEvent.ToolCallPart): boolean =>\n call.name === sendUserMessageName && Option.exists(messageText(call.params), ({ text }) => text.trim() !== \"\")\n\n/** @internal */\nexport const isTurnEnding = (call: AssistantEvent.ToolCallPart): boolean =>\n call.name === waitToolName || (call.name === sendUserMessageName && Option.isSome(endsWithMessage(call.params)))\n\n// From the call: a call cut off by a crash runs again with the same id, and finds its message already sent.\nconst messageId = AssistantToolCall.useSync((call) => Uuid.fromKey(`message:${call.callId}`))\n\nconst append = Effect.fnUntraced(function* (message: AssistantEvent.AssistantMessage) {\n const chat = yield* CurrentChat\n const entries = yield* chat.entries\n\n const sent = entries.some(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"AssistantMessage\") && event.id === message.id\n )\n\n if (!sent) yield* chat.append(message)\n})\n\nconst PlainMessage = Tool.make(sendUserMessageName, {\n description,\n parameters: Schema.Struct({ text: Schema.String, wait: WaitFlag }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, AssistantToolCall]\n})\n\nconst PlainTools = Toolkit.make(PlainMessage, Wait)\n\nconst plainHandlers = PlainTools.toHandlers({\n wait: () => Effect.succeed(\"waiting\"),\n send_user_message: Effect.fnUntraced(function* ({ text }) {\n yield* append(AssistantEvent.AssistantMessage.make({ id: yield* messageId, text }))\n\n return \"sent\"\n })\n})\n\nconst plainTools = Effect.gen(function* () {\n return yield* Effect.provideContext(PlainTools, yield* plainHandlers)\n})\n\n/** @internal */\nexport const toolResultKind = \"tool_result\"\n\nconst ToolResultRef = Schema.Struct({\n resultId: Schema.String.annotate({ description: \"The resultId of the tool result to show.\" })\n})\n\nconst decodeRef = Schema.decodeUnknownOption(ToolResultRef)\n\n// The latest successful result with that id, of a tool the kit shows.\nconst shownResult = Effect.fnUntraced(function* (resultId: string, shown: ReadonlySet<string>) {\n const entries = yield* (yield* CurrentChat).entries\n\n const result = entries\n .flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ToolResult\") && event.resultId === resultId\n ? [event]\n : []\n )\n .at(-1)\n\n if (result === undefined || result.outcome === \"failure\") {\n return yield* ToolFailure.fail(\n `No tool result ${resultId} to show: pass the resultId of a result you got in this chat.`,\n { reason: \"ResultNotFound\" }\n )\n }\n\n if (!shown.has(result.tool)) {\n return yield* ToolFailure.fail(`${result.tool} results can't be shown.`, { reason: \"ResultNotShown\" })\n }\n\n return result\n})\n\nconst blockTools = Effect.fnUntraced(function* (kit: BlockKit.BlockKit) {\n const asked = kit.blocks.filter((block) => !Block.isToolResult(block))\n const shown = new Set(kit.blocks.flatMap((block) => (Block.isToolResult(block) ? [block.tool.name] : [])))\n\n const catalog = [\n ...asked.map((block) => `- ${block.kind}: ${block.description}`),\n ...(shown.size === 0\n ? []\n : [\n `- ${toolResultKind}: a tool's result, shown the way the app draws it (found items as cards, a download as a button). Results you can show have a resultId: pass it as the props. From: ${[...shown].join(\", \")}.`\n ])\n ].join(\"\\n\")\n\n const kinds = [...asked.map((block) => block.kind), ...(shown.size === 0 ? [] : [toolResultKind])]\n const props = [...asked.map((block) => block.props), ...(shown.size === 0 ? [] : [ToolResultRef])]\n\n const BlockMessage = Tool.make(sendUserMessageName, {\n description: `${description}\nYou can add a block to show UI with the message, e.g. a few options to pick from or a yes/no, when that makes answering easier than typing. Plain text is fine too. Using it sends you an <answer>; the user may also just type their answer instead, and then you pass that message's id in closes.\nBlocks:\n${catalog}`,\n // Props are checked against their kind's schema by the handler; the model sees every kind's in the parameters.\n parameters: Schema.Struct({\n text: Schema.String,\n wait: WaitFlag,\n block: Schema.optional(\n Schema.Struct({\n kind: Schema.Literals(kinds),\n props: Schema.Union(props)\n })\n ),\n closes: Schema.optional(Schema.Array(Schema.String)).annotate({\n description:\n \"Ids of your earlier messages whose block the user just answered by typing instead of using it. Those blocks close. Leave it out otherwise: a block stays open when the user talks about something else.\"\n })\n }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, AssistantToolCall]\n })\n\n const BlockTools = Toolkit.make(BlockMessage, Wait)\n\n const handlers = yield* BlockTools.toHandlers({\n wait: () => Effect.succeed(\"waiting\"),\n send_user_message: Effect.fnUntraced(function* ({ text, block, closes }) {\n if (text.trim() === \"\") {\n return yield* ToolFailure.fail(\n \"The message has no text: the user sees only what you send here, and a block alone is an empty bubble. Send it again with the words they should read (a short lead-in for the block).\",\n { reason: \"EmptyMessage\" }\n )\n }\n\n const id = yield* messageId\n const closed = closes === undefined || closes.length === 0 ? {} : { closes }\n\n if (block === undefined) {\n yield* append(AssistantEvent.AssistantMessage.make({ id, text, ...closed }))\n\n return \"sent\"\n }\n\n if (block.kind === toolResultKind) {\n const ref = decodeRef(block.props)\n\n if (Option.isNone(ref)) {\n return yield* ToolFailure.fail(`A ${toolResultKind} block's props are { resultId }.`)\n }\n\n const result = yield* shownResult(ref.value.resultId, shown)\n\n yield* append(\n AssistantEvent.AssistantMessage.make({\n id,\n text,\n block: AssistantEvent.MessageBlock.make({ kind: result.tool, props: {}, resultId: ref.value.resultId }),\n ...closed\n })\n )\n\n return `sent as message ${id}`\n }\n\n const schema = kit.byKind.get(block.kind)?.props\n\n if (schema === undefined || Option.isNone(Schema.decodeUnknownOption(schema)(block.props))) {\n return yield* ToolFailure.fail(`Those props don't fit a ${block.kind} block.`)\n }\n\n yield* append(\n AssistantEvent.AssistantMessage.make({\n id,\n text,\n block: AssistantEvent.MessageBlock.make(block),\n ...closed\n })\n )\n\n return `sent as message ${id}`\n })\n })\n\n return yield* Effect.provideContext(BlockTools, handlers)\n})\n\n/** @internal */\nexport const makeMessaging: (kit: BlockKit.BlockKit | undefined) => Effect.Effect<Toolkits.Tools<CurrentChat>> =\n Effect.fnUntraced(function* (kit) {\n if (kit === undefined || kit.blocks.length === 0) {\n return Toolkits.fromWithHandler(yield* plainTools)\n }\n\n return Toolkits.fromWithHandler(yield* blockTools(kit))\n })\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,MAAa,eAAe;;AAG5B,MAAa,sBAAsB;AAEnC,MAAM,OAAO,KAAK,KAAK,cAAc;CACnC,aACE;CACF,SAAS,OAAO;AAClB,CAAC;AAED,MAAM,cACJ;AAEF,MAAM,WAAW,OAAO,QAAQ,SAAS,EACvC,aACE,oIACJ,CAAC;AAED,MAAM,kBAAkB,OAAO,oBAAoB,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,IAAI,EAAE,CAAC,CAAC;AAEhG,MAAM,cAAc,OAAO,oBAAoB,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,CAAC,CAAC;;AAIrF,MAAa,iBAAiB,SAC5B,KAAK,SAAA,uBAAgC,OAAO,OAAO,YAAY,KAAK,MAAM,IAAI,EAAE,WAAW,KAAK,KAAK,MAAM,EAAE;;AAG/G,MAAa,gBAAgB,SAC3B,KAAK,SAAA,UAA0B,KAAK,SAAA,uBAAgC,OAAO,OAAO,gBAAgB,KAAK,MAAM,CAAC;AAGhH,MAAM,YAAY,kBAAkB,SAAS,SAASA,QAAa,WAAW,KAAK,QAAQ,CAAC;AAE5F,MAAM,SAAS,OAAO,WAAW,WAAW,SAA0C;CACpF,MAAM,OAAO,OAAO;CAQpB,IAAI,EALS,OAFU,KAAK,QAAA,CAEP,MAClB,EAAE,YACDC,eAA8B,KAAK,KAAK,UAAU,SAAS,OAAO,kBAAkB,KAAK,MAAM,OAAO,QAAQ,EAG1G,GAAG,OAAO,KAAK,OAAO,OAAO;AACvC,CAAC;AAED,MAAM,eAAe,KAAK,KAAK,qBAAqB;CAClD;CACA,YAAY,OAAO,OAAO;EAAE,MAAM,OAAO;EAAQ,MAAM;CAAS,CAAC;CACjE,SAAS,OAAO;CAChB,SAASC;CACT,cAAc,CAAC,aAAa,iBAAiB;AAC/C,CAAC;AAED,MAAM,aAAa,QAAQ,KAAK,cAAc,IAAI;AAElD,MAAM,gBAAgB,WAAW,WAAW;CAC1C,YAAY,OAAO,QAAQ,SAAS;CACpC,mBAAmB,OAAO,WAAW,WAAW,EAAE,QAAQ;EACxD,OAAO,OAAA,iBAAuC,KAAK;GAAE,IAAI,OAAO;GAAW;EAAK,CAAC,CAAC;EAElF,OAAO;CACT,CAAC;AACH,CAAC;AAED,MAAM,aAAa,OAAO,IAAI,aAAa;CACzC,OAAO,OAAO,OAAO,eAAe,YAAY,OAAO,aAAa;AACtE,CAAC;;AAGD,MAAa,iBAAiB;AAE9B,MAAM,gBAAgB,OAAO,OAAO,EAClC,UAAU,OAAO,OAAO,SAAS,EAAE,aAAa,2CAA2C,CAAC,EAC9F,CAAC;AAED,MAAM,YAAY,OAAO,oBAAoB,aAAa;AAG1D,MAAM,cAAc,OAAO,WAAW,WAAW,UAAkB,OAA4B;CAG7F,MAAM,UAAS,QAFS,OAAO,YAAA,CAAa,QAAA,CAGzC,SAAS,EAAE,YACVD,eAA8B,KAAK,KAAK,UAAU,SAAS,OAAO,YAAY,KAAK,MAAM,aAAa,WAClG,CAAC,KAAK,IACN,CAAC,CACP,CAAC,CACA,GAAG,EAAE;CAER,IAAI,WAAW,KAAA,KAAa,OAAO,YAAY,WAC7C,OAAO,OAAOE,KACZ,kBAAkB,SAAS,gEAC3B,EAAE,QAAQ,iBAAiB,CAC7B;CAGF,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,GACxB,OAAO,OAAOA,KAAiB,GAAG,OAAO,KAAK,2BAA2B,EAAE,QAAQ,iBAAiB,CAAC;CAGvG,OAAO;AACT,CAAC;AAED,MAAM,aAAa,OAAO,WAAW,WAAW,KAAwB;CACtE,MAAM,QAAQ,IAAI,OAAO,QAAQ,UAAU,CAACC,aAAmB,KAAK,CAAC;CACrE,MAAM,QAAQ,IAAI,IAAI,IAAI,OAAO,SAAS,UAAWA,aAAmB,KAAK,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAE,CAAC;CAEzG,MAAM,UAAU,CACd,GAAG,MAAM,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa,GAC/D,GAAI,MAAM,SAAS,IACf,CAAC,IACD,CACE,KAAK,eAAe,sKAAsK,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,EAClN,CACN,CAAC,CAAC,KAAK,IAAI;CAEX,MAAM,QAAQ,CAAC,GAAG,MAAM,KAAK,UAAU,MAAM,IAAI,GAAG,GAAI,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,cAAc,CAAE;CACjG,MAAM,QAAQ,CAAC,GAAG,MAAM,KAAK,UAAU,MAAM,KAAK,GAAG,GAAI,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,aAAa,CAAE;CAEjG,MAAM,eAAe,KAAK,KAAK,qBAAqB;EAClD,aAAa,GAAG,YAAY;;;EAG9B;EAEE,YAAY,OAAO,OAAO;GACxB,MAAM,OAAO;GACb,MAAM;GACN,OAAO,OAAO,SACZ,OAAO,OAAO;IACZ,MAAM,OAAO,SAAS,KAAK;IAC3B,OAAO,OAAO,MAAM,KAAK;GAC3B,CAAC,CACH;GACA,QAAQ,OAAO,SAAS,OAAO,MAAM,OAAO,MAAM,CAAC,CAAC,CAAC,SAAS,EAC5D,aACE,0MACJ,CAAC;EACH,CAAC;EACD,SAAS,OAAO;EAChB,SAASF;EACT,cAAc,CAAC,aAAa,iBAAiB;CAC/C,CAAC;CAED,MAAM,aAAa,QAAQ,KAAK,cAAc,IAAI;CAElD,MAAM,WAAW,OAAO,WAAW,WAAW;EAC5C,YAAY,OAAO,QAAQ,SAAS;EACpC,mBAAmB,OAAO,WAAW,WAAW,EAAE,MAAM,OAAO,UAAU;GACvE,IAAI,KAAK,KAAK,MAAM,IAClB,OAAO,OAAOC,KACZ,wLACA,EAAE,QAAQ,eAAe,CAC3B;GAGF,MAAM,KAAK,OAAO;GAClB,MAAM,SAAS,WAAW,KAAA,KAAa,OAAO,WAAW,IAAI,CAAC,IAAI,EAAE,OAAO;GAE3E,IAAI,UAAU,KAAA,GAAW;IACvB,OAAO,OAAA,iBAAuC,KAAK;KAAE;KAAI;KAAM,GAAG;IAAO,CAAC,CAAC;IAE3E,OAAO;GACT;GAEA,IAAI,MAAM,SAAA,eAAyB;IACjC,MAAM,MAAM,UAAU,MAAM,KAAK;IAEjC,IAAI,OAAO,OAAO,GAAG,GACnB,OAAO,OAAOA,KAAiB,KAAK,eAAe,iCAAiC;IAGtF,MAAM,SAAS,OAAO,YAAY,IAAI,MAAM,UAAU,KAAK;IAE3D,OAAO,OAAA,iBAC2B,KAAK;KACnC;KACA;KACA,OAAA,aAAmC,KAAK;MAAE,MAAM,OAAO;MAAM,OAAO,CAAC;MAAG,UAAU,IAAI,MAAM;KAAS,CAAC;KACtG,GAAG;IACL,CAAC,CACH;IAEA,OAAO,mBAAmB;GAC5B;GAEA,MAAM,SAAS,IAAI,OAAO,IAAI,MAAM,IAAI,CAAC,EAAE;GAE3C,IAAI,WAAW,KAAA,KAAa,OAAO,OAAO,OAAO,oBAAoB,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,GACvF,OAAO,OAAOA,KAAiB,2BAA2B,MAAM,KAAK,QAAQ;GAG/E,OAAO,OAAA,iBAC2B,KAAK;IACnC;IACA;IACA,OAAA,aAAmC,KAAK,KAAK;IAC7C,GAAG;GACL,CAAC,CACH;GAEA,OAAO,mBAAmB;EAC5B,CAAC;CACH,CAAC;CAED,OAAO,OAAO,OAAO,eAAe,YAAY,QAAQ;AAC1D,CAAC;;AAGD,MAAa,gBACX,OAAO,WAAW,WAAW,KAAK;CAChC,IAAI,QAAQ,KAAA,KAAa,IAAI,OAAO,WAAW,GAC7C,OAAOE,gBAAyB,OAAO,UAAU;CAGnD,OAAOA,gBAAyB,OAAO,WAAW,GAAG,CAAC;AACxD,CAAC"}
|
|
@@ -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;;
|
|
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;;qBA8NW,MAAG;WAEW,cAAA;WACL,SAAA;WACH,MAAA;WACI,UAAA,QAAQ,QAAQ,eAAyB,cAAc;WAEtD,WAAA,OAAO;MA8D9B,OAAA"}
|