effect-assistant 0.6.0 → 0.7.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantLoop.js","names":["Loop.value","Loop.next","Loop.stop","Loop.loop","Loop.TurnTools","Toolkits.empty","Toolkits.fromToolkit","Toolkits.merge","Loop.step","Loop.run"],"sources":["../src/AssistantLoop.ts"],"sourcesContent":["/**\n * Runs the agent loop, which calls the model, runs its tool calls, and repeats.\n *\n * Most handlers use `turn.run` or `run.loop`. This module is for a loop of your own: {@link run}\n * runs the loop until the model is done, and {@link step} is one step, as a body for\n * {@link loop}.\n *\n * Tools are a native toolkit (`effect/ai/Toolkit`) whose handlers come from the handler's layer.\n * In a turn, the assistant's own tools (`send_user_message`, `wait`, and the specialist tools)\n * are added to them.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\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 Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport * as Loop from \"./internal/loop.ts\"\nimport * as Toolkits from \"./internal/toolkit.ts\"\n\n/**\n * Represents what the loop emits.\n *\n * For each model call, the loop emits the call's output. Then, for each of its tool calls, it\n * emits a `ToolStarted` before the call runs and the call's result.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Event = Loop.Event\n\n/**\n * Represents a chat's history as the model reads it.\n *\n * It holds the messages, and the last `sequence` of the chat they cover.\n *\n * @category models\n * @since 0.1.0\n */\nexport type ChatHistory = Loop.ChatHistory\n\n/**\n * Represents how a step went, for deciding what comes next.\n *\n * @category models\n * @since 0.1.0\n */\nexport type StepResult = Loop.StepResult\n\n/**\n * Represents what a loop body emits, which is either a value or what to do next.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Step<A, S> = Loop.Step<A, S>\n\n/**\n * Emits a value from a loop body.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const value = Loop.value\n\n/**\n * Ends a loop body's step and continues the loop with the given state.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const next = Loop.next\n\n/**\n * Ends the loop.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const stop = Loop.stop\n\n/**\n * Runs a loop body from an initial state.\n *\n * It emits the body's values, and runs the body again with the state it passes to `next`, until\n * the body calls `stop`.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const loop = Loop.loop\n\ntype NoTools = Record<never, never>\n\n/**\n * Extracts the services a toolkit's tools need when they run, which are their handlers and the\n * services those handlers use.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolkitServices<T extends Record<string, Tool.Any>> = Tool.HandlersFor<T> | Toolkits.ToolServices<T>\n\n/**\n * Represents the options for {@link step}.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface StepOptions<T extends Record<string, Tool.Any> = NoTools> {\n /** The full system prompt, like `turn.instructions(text)`, optionally with span attributes. */\n readonly instructions: string | Instructions.Resolved\n /**\n * The tools the model can call, in addition to the assistant's own tools in a turn. Their\n * handlers come from the layer.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /**\n * The current history, read at the start of the step. It is either messages, or a chat's\n * history (`turn.history()`), whose `throughSequence` the step's `ModelOutput` records as how far\n * the model had read.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message> | ChatHistory>\n /**\n * Defaults to `\"auto\"`, where a step without a tool call ends the loop. Forcing a call\n * (`\"required\"`, or a tool by name) is refused by newer models, and with extended thinking.\n */\n readonly toolChoice?: LanguageModel.ToolChoice<string> | undefined\n}\n\n/**\n * Represents the options for {@link run}.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Options<T extends Record<string, Tool.Any> = NoTools> extends StepOptions<T> {\n /** The model calls allowed. Defaults to 16. */\n readonly maxSteps?: number | undefined\n}\n\nconst toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T> | undefined) =>\n Effect.map(Loop.TurnTools, (turn) => {\n const own = toolkit === undefined ? Toolkits.empty : Toolkits.fromToolkit(toolkit)\n\n return Option.match(turn, { onNone: () => own, onSome: (builtins) => Toolkits.merge(builtins, own) })\n })\n\n/**\n * Runs one step of the agent loop, as a body for {@link loop}.\n *\n * **Details**\n *\n * The step calls the model with the current history and emits its `ModelOutput`. It then runs\n * the output's tool calls and emits a `ToolStarted` and a `ToolResult` for each. `then` receives\n * the step's result and decides what comes next, with `AssistantLoop.next(state)` or\n * `AssistantLoop.stop()`.\n *\n * In a turn's loop, a call to a tool with Effect's `needsApproval` doesn't run: the step emits an\n * `ApprovalRequested` for it, and ends the turn, which waits for the user (see `ToolApproval`). A\n * call the user approved runs at the start of the next step, before the model is called. A\n * specialist's run can't wait for the user: such a call fails, and the run reads why.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const step = <T extends Record<string, Tool.Any> = NoTools, S = never, E2 = never, R2 = never>(\n options: StepOptions<T>,\n then: (result: StepResult) => Stream.Stream<Step<never, S>, E2, R2>\n): Stream.Stream<Step<Event, S>, AiError.AiError | E2, LanguageModel.LanguageModel | ToolkitServices<T> | R2> =>\n Stream.unwrap(Effect.map(toolsOf(options.toolkit), (toolkit) => Loop.step({ ...options, toolkit }, then)))\n\n/**\n * Runs the agent loop as a stream.\n *\n * **Details**\n *\n * It runs {@link step} after step until the model is done and nothing new arrived while it\n * worked, or until it has made `maxSteps` model calls.\n *\n * **Example** (A turn's loop with a step limit)\n *\n * ```ts\n * import { AssistantLoop } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n * import * as Stream from \"effect/Stream\"\n *\n * const instructions = \"You help a small marketing team.\"\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * AssistantLoop.run({\n * instructions: turn.instructions(instructions),\n * toolkit: MyTools, // the assistant's own tools are added to them\n * history: turn.history(),\n * maxSteps: 12\n * }).pipe(Stream.concat(turn.replyIfOwed({ instructions })))\n * ).pipe(Layer.provide(MyToolsLive))\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const run = <T extends Record<string, Tool.Any> = NoTools>(\n options: Options<T>\n): Stream.Stream<Event, AiError.AiError, LanguageModel.LanguageModel | ToolkitServices<T>> =>\n Stream.unwrap(Effect.map(toolsOf(options.toolkit), (toolkit) => Loop.run({ ...options, toolkit })))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAoEA,MAAa,QAAQA;;;;;;;AAQrB,MAAa,OAAOC;;;;;;;AAQpB,MAAa,OAAOC;;;;;;;;;;AAWpB,MAAa,OAAOC;AAmDpB,MAAM,WAA+C,YACnD,OAAO,IAAIC,YAAiB,SAAS;CACnC,MAAM,MAAM,YAAY,KAAA,IAAYC,QAAiBC,YAAqB,OAAO;CAEjF,OAAO,OAAO,MAAM,MAAM;EAAE,cAAc;EAAK,SAAS,aAAaC,MAAe,UAAU,GAAG;CAAE,CAAC;AACtG,CAAC;;;;;;;;;;;;;;;;;;;AAoBH,MAAa,QACX,SACA,SAEA,OAAO,OAAO,OAAO,IAAI,QAAQ,QAAQ,OAAO,IAAI,YAAYC,OAAU;CAAE,GAAG;CAAS;AAAQ,GAAG,IAAI,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC3G,MAAa,OACX,YAEA,OAAO,OAAO,OAAO,IAAI,QAAQ,QAAQ,OAAO,IAAI,YAAYC,MAAS;CAAE,GAAG;CAAS;AAAQ,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"AssistantLoop.js","names":["Loop.value","Loop.next","Loop.stop","Loop.loop","Loop.TurnTools","Toolkits.empty","Toolkits.fromToolkit","Toolkits.merge","Loop.step","Loop.run"],"sources":["../src/AssistantLoop.ts"],"sourcesContent":["/**\n * Runs the agent loop, which calls the model, runs its tool calls, and repeats.\n *\n * Most handlers use `turn.run`, in an assistant or a specialist. This module is for a loop of your own: {@link run}\n * runs the loop until the model is done, and {@link step} is one step, as a body for\n * {@link loop}.\n *\n * Tools are a native toolkit (`effect/ai/Toolkit`) whose handlers come from the handler's layer.\n * In a turn, the assistant's own tools (`send_user_message`, `wait`, and the specialist tools)\n * are added to them.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\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 Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport * as Loop from \"./internal/loop.ts\"\nimport * as Toolkits from \"./internal/toolkit.ts\"\n\n/**\n * Represents what the loop emits.\n *\n * For each model call, the loop emits the call's output. Then, for each of its tool calls, it\n * emits a `ToolStarted` before the call runs and the call's result.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Event = Loop.Event\n\n/**\n * Represents a chat's history as the model reads it.\n *\n * It holds the messages, and the last `sequence` of the chat they cover.\n *\n * @category models\n * @since 0.1.0\n */\nexport type ChatHistory = Loop.ChatHistory\n\n/**\n * Represents how a step went, for deciding what comes next.\n *\n * @category models\n * @since 0.1.0\n */\nexport type StepResult = Loop.StepResult\n\n/**\n * Represents what a loop body emits, which is either a value or what to do next.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Step<A, S> = Loop.Step<A, S>\n\n/**\n * Emits a value from a loop body.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const value = Loop.value\n\n/**\n * Ends a loop body's step and continues the loop with the given state.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const next = Loop.next\n\n/**\n * Ends the loop.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const stop = Loop.stop\n\n/**\n * Runs a loop body from an initial state.\n *\n * It emits the body's values, and runs the body again with the state it passes to `next`, until\n * the body calls `stop`.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const loop = Loop.loop\n\ntype NoTools = Record<never, never>\n\n/**\n * Extracts the services a toolkit's tools need when they run, which are their handlers and the\n * services those handlers use.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolkitServices<T extends Record<string, Tool.Any>> = Tool.HandlersFor<T> | Toolkits.ToolServices<T>\n\n/**\n * Represents the options for {@link step}.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface StepOptions<T extends Record<string, Tool.Any> = NoTools> {\n /** The full system prompt, like `turn.instructions(text)`, optionally with span attributes. */\n readonly instructions: string | Instructions.Resolved\n /**\n * The tools the model can call, in addition to the assistant's own tools in a turn. Their\n * handlers come from the layer.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /**\n * The current history, read at the start of the step. It is either messages, or a chat's\n * history (`turn.history()`), whose `throughSequence` the step's `ModelOutput` records as how far\n * the model had read.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message> | ChatHistory>\n /**\n * Defaults to `\"auto\"`, where a step without a tool call ends the loop. Forcing a call\n * (`\"required\"`, or a tool by name) is refused by newer models, and with extended thinking.\n */\n readonly toolChoice?: LanguageModel.ToolChoice<string> | undefined\n}\n\n/**\n * Represents the options for {@link run}.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Options<T extends Record<string, Tool.Any> = NoTools> extends StepOptions<T> {\n /** The model calls allowed. Defaults to 16. */\n readonly maxSteps?: number | undefined\n}\n\nconst toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T> | undefined) =>\n Effect.map(Loop.TurnTools, (turn) => {\n const own = toolkit === undefined ? Toolkits.empty : Toolkits.fromToolkit(toolkit)\n\n return Option.match(turn, { onNone: () => own, onSome: (builtins) => Toolkits.merge(builtins, own) })\n })\n\n/**\n * Runs one step of the agent loop, as a body for {@link loop}.\n *\n * **Details**\n *\n * The step calls the model with the current history and emits its `ModelOutput`. It then runs\n * the output's tool calls and emits a `ToolStarted` and a `ToolResult` for each. `then` receives\n * the step's result and decides what comes next, with `AssistantLoop.next(state)` or\n * `AssistantLoop.stop()`.\n *\n * In a turn's loop, a call to a tool with Effect's `needsApproval` doesn't run: the step emits an\n * `ApprovalRequested` for it, and ends the turn, which waits for the user (see `ToolApproval`). A\n * call the user approved runs at the start of the next step, before the model is called. A\n * specialist's run can't wait for the user: such a call fails, and the run reads why.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const step = <T extends Record<string, Tool.Any> = NoTools, S = never, E2 = never, R2 = never>(\n options: StepOptions<T>,\n then: (result: StepResult) => Stream.Stream<Step<never, S>, E2, R2>\n): Stream.Stream<Step<Event, S>, AiError.AiError | E2, LanguageModel.LanguageModel | ToolkitServices<T> | R2> =>\n Stream.unwrap(Effect.map(toolsOf(options.toolkit), (toolkit) => Loop.step({ ...options, toolkit }, then)))\n\n/**\n * Runs the agent loop as a stream.\n *\n * **Details**\n *\n * It runs {@link step} after step until the model is done and nothing new arrived while it\n * worked, or until it has made `maxSteps` model calls.\n *\n * **Example** (A turn's loop with a step limit)\n *\n * ```ts\n * import { AssistantLoop } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n * import * as Stream from \"effect/Stream\"\n *\n * const instructions = \"You help a small marketing team.\"\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * AssistantLoop.run({\n * instructions: turn.instructions(instructions),\n * toolkit: MyTools, // the assistant's own tools are added to them\n * history: turn.history(),\n * maxSteps: 12\n * }).pipe(Stream.concat(turn.replyIfOwed({ instructions })))\n * ).pipe(Layer.provide(MyToolsLive))\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const run = <T extends Record<string, Tool.Any> = NoTools>(\n options: Options<T>\n): Stream.Stream<Event, AiError.AiError, LanguageModel.LanguageModel | ToolkitServices<T>> =>\n Stream.unwrap(Effect.map(toolsOf(options.toolkit), (toolkit) => Loop.run({ ...options, toolkit })))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAoEA,MAAa,QAAQA;;;;;;;AAQrB,MAAa,OAAOC;;;;;;;AAQpB,MAAa,OAAOC;;;;;;;;;;AAWpB,MAAa,OAAOC;AAmDpB,MAAM,WAA+C,YACnD,OAAO,IAAIC,YAAiB,SAAS;CACnC,MAAM,MAAM,YAAY,KAAA,IAAYC,QAAiBC,YAAqB,OAAO;CAEjF,OAAO,OAAO,MAAM,MAAM;EAAE,cAAc;EAAK,SAAS,aAAaC,MAAe,UAAU,GAAG;CAAE,CAAC;AACtG,CAAC;;;;;;;;;;;;;;;;;;;AAoBH,MAAa,QACX,SACA,SAEA,OAAO,OAAO,OAAO,IAAI,QAAQ,QAAQ,OAAO,IAAI,YAAYC,OAAU;CAAE,GAAG;CAAS;AAAQ,GAAG,IAAI,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC3G,MAAa,OACX,YAEA,OAAO,OAAO,OAAO,IAAI,QAAQ,QAAQ,OAAO,IAAI,YAAYC,MAAS;CAAE,GAAG;CAAS;AAAQ,CAAC,CAAC,CAAC"}
package/dist/Skill.d.ts CHANGED
@@ -131,7 +131,7 @@ export interface Toolkit<R = never> extends AiToolkit.Toolkit<{
131
131
  * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
132
132
  *
133
133
  * return turn
134
- * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
134
+ * .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
135
135
  * .pipe(Stream.provide(skills.layer))
136
136
  * })
137
137
  * )
package/dist/Skill.js CHANGED
@@ -116,7 +116,7 @@ function toolkitOf(skills) {
116
116
  * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
117
117
  *
118
118
  * return turn
119
- * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
119
+ * .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
120
120
  * .pipe(Stream.provide(skills.layer))
121
121
  * })
122
122
  * )
package/dist/Skill.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Skill.js","names":["ToolFailure.ToolFailure","ToolFailure.fail","Instructions.resolve"],"sources":["../src/Skill.ts"],"sourcesContent":["/**\n * Defines skills, which are playbooks with step-by-step instructions for a kind of task.\n *\n * A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its\n * description is the catalog, each skill's name and when to use it, and the model calls it to read\n * a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.\n *\n * @since 0.1.0\n */\nimport * as Tool from \"effect/ai/Tool\"\nimport * as AiToolkit from \"effect/ai/Toolkit\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Instructions from \"./Instructions.ts\"\nimport * as ToolFailure from \"./ToolFailure.ts\"\n\n/**\n * Represents a skill.\n *\n * `R` is the services its instructions need when they're loaded.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Skill<R = never> {\n readonly name: string\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n}\n\n/**\n * Creates a skill from its name, a description of when to use it, and its instructions.\n *\n * **Details**\n *\n * Write the description as a trigger (\"Use when…\"), since it's all the model sees until it loads\n * the skill. The instructions are text, or an Effect that is resolved each time the skill is\n * loaded. Offer skills to a loop with {@link toolkit}.\n *\n * **Example** (Defining a playbook for ordering campaigns)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n *\n * const orderingCampaigns = Skill.make(\"Ordering a campaign\", {\n * description: \"Use when the user wants to order or start a campaign.\",\n * instructions: `1. Pick the product with list_campaign_products.\n * 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.\n * 3. When you have both, call start_campaign.`\n * })\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <R = never>(\n name: string,\n options: {\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n }\n): Skill<R> => ({ name, description: options.description, instructions: options.instructions })\n\n/**\n * Extracts the services that skills need when they're loaded.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Services<S> = S extends Skill<infer R> ? R : never\n\nconst loadSkill = (description: string) =>\n Tool.make(\"load_skill\", {\n description,\n parameters: Schema.Struct({ name: Schema.String }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure\n })\n\n/**\n * Represents the `load_skill` tool, which reads a skill's instructions by name.\n *\n * @category models\n * @since 0.4.0\n */\nexport type LoadSkill = ReturnType<typeof loadSkill>\n\n/**\n * Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.\n *\n * `R` is the services the skills' instructions need, which the layer requires.\n *\n * @category models\n * @since 0.4.0\n */\nexport interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {\n /** The handler of `load_skill`, which resolves the instructions of these skills. */\n readonly layer: Layer.Layer<Tool.Handler<\"load_skill\">, never, R>\n}\n\nconst catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>\n [\n \"Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.\",\n \"\",\n \"Skills:\",\n ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)\n ].join(\"\\n\")\n\nfunction toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n): Toolkit<Services<Skills[number]>>\n// The overload states what the layer requires: the services of these skills' instructions.\nfunction toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {\n const byName = new Map(skills.map((skill) => [skill.name, skill]))\n const tools = AiToolkit.make(loadSkill(catalog(skills)))\n\n const layer = tools.toLayer(\n Effect.gen(function* () {\n const context = yield* Effect.context<unknown>()\n\n return {\n load_skill: Effect.fnUntraced(function* ({ name }) {\n const skill = byName.get(name)\n\n if (skill === undefined) {\n return yield* ToolFailure.fail(\n `No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(\", \")}.`\n )\n }\n\n const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))\n\n return resolved.text\n })\n }\n })\n )\n\n return Object.assign(tools, { layer })\n}\n\n/**\n * Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each\n * skill's name and when to use it, and its handler's `layer`.\n *\n * **Details**\n *\n * Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires\n * the services the skills' instructions need. The model loads a skill by name; a name that isn't\n * in the list is a failure the model reads, with the names there are.\n *\n * The handler resolves only the skills of its own list. To offer each user the skills they may\n * use, build the toolkit in the turn from that user's list, and provide its layer to the loop's\n * stream (`Stream.provide(skills.layer)`): building one does no work.\n *\n * **Example** (Giving an assistant its playbooks)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })\n * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))\n * ```\n *\n * **Example** (Offering each user the skills their role allows)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n * import * as Stream from \"effect/Stream\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * Stream.unwrap(\n * Effect.map(roleOf(turn.chatId), (role) => {\n * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))\n *\n * return turn\n * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })\n * .pipe(Stream.provide(skills.layer))\n * })\n * )\n * )\n * ```\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const toolkit: <const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n) => Toolkit<Services<Skills[number]>> = toolkitOf\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,MAAa,QACX,MACA,aAMc;CAAE;CAAM,aAAa,QAAQ;CAAa,cAAc,QAAQ;AAAa;AAU7F,MAAM,aAAa,gBACjB,KAAK,KAAK,cAAc;CACtB;CACA,YAAY,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,CAAC;CACjD,SAAS,OAAO;CAChB,SAASA;AACX,CAAC;AAuBH,MAAM,WAAW,WACf;CACE;CACA;CACA;CACA,GAAG,OAAO,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa;AAClE,CAAC,CAAC,KAAK,IAAI;AAMb,SAAS,UAAU,QAAyD;CAC1E,MAAM,SAAS,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACjE,MAAM,QAAQ,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;CAEvD,MAAM,QAAQ,MAAM,QAClB,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,OAAO,QAAiB;EAE/C,OAAO,EACL,YAAY,OAAO,WAAW,WAAW,EAAE,QAAQ;GACjD,MAAM,QAAQ,OAAO,IAAI,IAAI;GAE7B,IAAI,UAAU,KAAA,GACZ,OAAO,OAAOC,KACZ,kBAAkB,KAAK,oBAAoB,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,EAC1F;GAKF,QAAO,OAFiBC,QAAqB,MAAM,YAAY,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,CAAC,EAAA,CAEpF;EAClB,CAAC,EACH;CACF,CAAC,CACH;CAEA,OAAO,OAAO,OAAO,OAAO,EAAE,MAAM,CAAC;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,UAE4B"}
1
+ {"version":3,"file":"Skill.js","names":["ToolFailure.ToolFailure","ToolFailure.fail","Instructions.resolve"],"sources":["../src/Skill.ts"],"sourcesContent":["/**\n * Defines skills, which are playbooks with step-by-step instructions for a kind of task.\n *\n * A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its\n * description is the catalog, each skill's name and when to use it, and the model calls it to read\n * a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.\n *\n * @since 0.1.0\n */\nimport * as Tool from \"effect/ai/Tool\"\nimport * as AiToolkit from \"effect/ai/Toolkit\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Instructions from \"./Instructions.ts\"\nimport * as ToolFailure from \"./ToolFailure.ts\"\n\n/**\n * Represents a skill.\n *\n * `R` is the services its instructions need when they're loaded.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Skill<R = never> {\n readonly name: string\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n}\n\n/**\n * Creates a skill from its name, a description of when to use it, and its instructions.\n *\n * **Details**\n *\n * Write the description as a trigger (\"Use when…\"), since it's all the model sees until it loads\n * the skill. The instructions are text, or an Effect that is resolved each time the skill is\n * loaded. Offer skills to a loop with {@link toolkit}.\n *\n * **Example** (Defining a playbook for ordering campaigns)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n *\n * const orderingCampaigns = Skill.make(\"Ordering a campaign\", {\n * description: \"Use when the user wants to order or start a campaign.\",\n * instructions: `1. Pick the product with list_campaign_products.\n * 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.\n * 3. When you have both, call start_campaign.`\n * })\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <R = never>(\n name: string,\n options: {\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n }\n): Skill<R> => ({ name, description: options.description, instructions: options.instructions })\n\n/**\n * Extracts the services that skills need when they're loaded.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Services<S> = S extends Skill<infer R> ? R : never\n\nconst loadSkill = (description: string) =>\n Tool.make(\"load_skill\", {\n description,\n parameters: Schema.Struct({ name: Schema.String }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure\n })\n\n/**\n * Represents the `load_skill` tool, which reads a skill's instructions by name.\n *\n * @category models\n * @since 0.4.0\n */\nexport type LoadSkill = ReturnType<typeof loadSkill>\n\n/**\n * Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.\n *\n * `R` is the services the skills' instructions need, which the layer requires.\n *\n * @category models\n * @since 0.4.0\n */\nexport interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {\n /** The handler of `load_skill`, which resolves the instructions of these skills. */\n readonly layer: Layer.Layer<Tool.Handler<\"load_skill\">, never, R>\n}\n\nconst catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>\n [\n \"Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.\",\n \"\",\n \"Skills:\",\n ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)\n ].join(\"\\n\")\n\nfunction toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n): Toolkit<Services<Skills[number]>>\n// The overload states what the layer requires: the services of these skills' instructions.\nfunction toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {\n const byName = new Map(skills.map((skill) => [skill.name, skill]))\n const tools = AiToolkit.make(loadSkill(catalog(skills)))\n\n const layer = tools.toLayer(\n Effect.gen(function* () {\n const context = yield* Effect.context<unknown>()\n\n return {\n load_skill: Effect.fnUntraced(function* ({ name }) {\n const skill = byName.get(name)\n\n if (skill === undefined) {\n return yield* ToolFailure.fail(\n `No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(\", \")}.`\n )\n }\n\n const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))\n\n return resolved.text\n })\n }\n })\n )\n\n return Object.assign(tools, { layer })\n}\n\n/**\n * Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each\n * skill's name and when to use it, and its handler's `layer`.\n *\n * **Details**\n *\n * Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires\n * the services the skills' instructions need. The model loads a skill by name; a name that isn't\n * in the list is a failure the model reads, with the names there are.\n *\n * The handler resolves only the skills of its own list. To offer each user the skills they may\n * use, build the toolkit in the turn from that user's list, and provide its layer to the loop's\n * stream (`Stream.provide(skills.layer)`): building one does no work.\n *\n * **Example** (Giving an assistant its playbooks)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })\n * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))\n * ```\n *\n * **Example** (Offering each user the skills their role allows)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n * import * as Stream from \"effect/Stream\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * Stream.unwrap(\n * Effect.map(roleOf(turn.chatId), (role) => {\n * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))\n *\n * return turn\n * .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })\n * .pipe(Stream.provide(skills.layer))\n * })\n * )\n * )\n * ```\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const toolkit: <const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n) => Toolkit<Services<Skills[number]>> = toolkitOf\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,MAAa,QACX,MACA,aAMc;CAAE;CAAM,aAAa,QAAQ;CAAa,cAAc,QAAQ;AAAa;AAU7F,MAAM,aAAa,gBACjB,KAAK,KAAK,cAAc;CACtB;CACA,YAAY,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,CAAC;CACjD,SAAS,OAAO;CAChB,SAASA;AACX,CAAC;AAuBH,MAAM,WAAW,WACf;CACE;CACA;CACA;CACA,GAAG,OAAO,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa;AAClE,CAAC,CAAC,KAAK,IAAI;AAMb,SAAS,UAAU,QAAyD;CAC1E,MAAM,SAAS,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACjE,MAAM,QAAQ,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;CAEvD,MAAM,QAAQ,MAAM,QAClB,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,OAAO,QAAiB;EAE/C,OAAO,EACL,YAAY,OAAO,WAAW,WAAW,EAAE,QAAQ;GACjD,MAAM,QAAQ,OAAO,IAAI,IAAI;GAE7B,IAAI,UAAU,KAAA,GACZ,OAAO,OAAOC,KACZ,kBAAkB,KAAK,oBAAoB,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,EAC1F;GAKF,QAAO,OAFiBC,QAAqB,MAAM,YAAY,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,CAAC,EAAA,CAEpF;EAClB,CAAC,EACH;CACF,CAAC,CACH;CAEA,OAAO,OAAO,OAAO,OAAO,EAAE,MAAM,CAAC;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,UAE4B"}
@@ -15,7 +15,7 @@ import * as Toolkit from "effect/ai/Toolkit";
15
15
  import * as LanguageModel from "effect/ai/LanguageModel";
16
16
  import * as AiError from "effect/ai/AiError";
17
17
  declare namespace Specialist_d_exports {
18
- export { Any, Brief, Built, CheckServices, Handler, HandlerOf, LoopOptions, Run, RunServices, Runner, SpawnRequest, Specialist, SpecialistError, SpecialistHandler, ToolHandlers, make };
18
+ export { Any, Brief, Built, CheckServices, Handler, HandlerOf, RunOptions, Runner, SpawnRequest, Specialist, SpecialistError, SpecialistHandler, ToolHandlers, Turn, TurnServices, make };
19
19
  }
20
20
  declare const TypeId = "~effect-assistant/Specialist";
21
21
  /**
@@ -32,10 +32,10 @@ export interface Specialist<Name extends string = string> {
32
32
  /** The key of the specialist's handler, as `toLayer` provides it. */
33
33
  readonly Handler: Context.Key<SpecialistHandler<Name>, Built>;
34
34
  /**
35
- * Provides the specialist's handler. The handler may use the {@link RunServices} and the
35
+ * Provides the specialist's handler. The handler may use the {@link TurnServices} and the
36
36
  * services its build Effect required (see {@link CheckServices}).
37
37
  */
38
- readonly toLayer: <R = never, RE = never, E = never, RX = never>(build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) & CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>;
38
+ readonly toLayer: <R = never, RE = never, E = never, RX = never>(build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) & CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>;
39
39
  }
40
40
  /**
41
41
  * Represents the check `toLayer` applies to the services a handler uses.
@@ -89,20 +89,20 @@ export interface SpecialistHandler<Name extends string> {
89
89
  */
90
90
  export type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never;
91
91
  /**
92
- * The services a run provides to its handler, which are its chat, the chat's runner, and the
92
+ * The services a turn provides to its handler, which are its chat, the chat's runner, and the
93
93
  * model.
94
94
  *
95
95
  * @category utility types
96
96
  * @since 0.1.0
97
97
  */
98
- export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel;
98
+ export type TurnServices = CurrentChat | Runner | LanguageModel.LanguageModel;
99
99
  /**
100
- * Represents the options for `run.loop`.
100
+ * Represents the options for `turn.run`.
101
101
  *
102
102
  * @category models
103
103
  * @since 0.1.0
104
104
  */
105
- export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
105
+ export interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
106
106
  readonly instructions: string | Resolved;
107
107
  /**
108
108
  * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
@@ -113,12 +113,13 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
113
113
  readonly maxSteps?: number | undefined;
114
114
  }
115
115
  /**
116
- * Represents one run of a specialist on a brief, as its handler sees it.
116
+ * Represents one turn of a specialist's run, as its handler sees it. The first turn works on the
117
+ * brief, and each later one on the messages sent to the run since (follow-ups and steering).
117
118
  *
118
119
  * @category models
119
120
  * @since 0.1.0
120
121
  */
121
- export interface Run {
122
+ export interface Turn {
122
123
  readonly runId: RunId;
123
124
  /** The chat the run belongs to, which is the chat of the turn that spawned it. */
124
125
  readonly chatId: ChatId;
@@ -129,7 +130,7 @@ export interface Run {
129
130
  */
130
131
  readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>;
131
132
  /** Runs the agent loop over the run's history, with the given instructions and tools. */
132
- readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(options: LoopOptions<T>) => Stream.Stream<Event, AiError.AiError, LanguageModel.LanguageModel | ToolkitServices<T>>;
133
+ readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(options: RunOptions<T>) => Stream.Stream<Event, AiError.AiError, LanguageModel.LanguageModel | ToolkitServices<T>>;
133
134
  }
134
135
  /**
135
136
  * Represents a specialist's handler, which works on a brief and returns a stream of what the loop
@@ -141,7 +142,7 @@ export interface Run {
141
142
  * @category models
142
143
  * @since 0.1.0
143
144
  */
144
- export type Handler<R = never, RE = R> = (run: Run) => Stream.Stream<Event, unknown, R> | Effect.Effect<Stream.Stream<Event, unknown, R>, unknown, RE>;
145
+ export type Handler<R = never, RE = R> = (turn: Turn) => Stream.Stream<Event, unknown, R> | Effect.Effect<Stream.Stream<Event, unknown, R>, unknown, RE>;
145
146
  /**
146
147
  * Represents a specialist's handler as its layer provides it.
147
148
  *
@@ -152,14 +153,14 @@ export type Handler<R = never, RE = R> = (run: Run) => Stream.Stream<Event, unkn
152
153
  * @since 0.1.0
153
154
  */
154
155
  export interface Built {
155
- readonly open: Effect.Effect<(run: Run) => Stream.Stream<Event, unknown, RunServices>, never, Scope.Scope | CurrentChat>;
156
+ readonly open: Effect.Effect<(turn: Turn) => Stream.Stream<Event, unknown, TurnServices>, never, Scope.Scope | CurrentChat>;
156
157
  }
157
158
  /**
158
159
  * Creates a specialist from its name and a description of when to use it.
159
160
  *
160
161
  * **Details**
161
162
  *
162
- * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the
163
+ * The specialist's `toLayer` provides its handler, which is usually `turn.run` with the
163
164
  * specialist's own instructions and tools. List the specialist in `Assistant.make`'s
164
165
  * `specialists`, and provide its layer to the assistant's.
165
166
  *
@@ -173,8 +174,8 @@ export interface Built {
173
174
  * description: "Researches past campaigns and orders. Give it the question and any context."
174
175
  * })
175
176
  *
176
- * const ResearchLive = Research.toLayer((run) =>
177
- * run.loop({
177
+ * const ResearchLive = Research.toLayer((turn) =>
178
+ * turn.run({
178
179
  * instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
179
180
  * toolkit: ResearchTools
180
181
  * })
@@ -1 +1 @@
1
- {"version":3,"file":"Specialist.d.ts","names":[],"sources":["../src/Specialist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;cA0BM;;;;;;;iBAQW,WAAW;YAChB,gBAAgB;WACjB,MAAM;;WAEN;;WAEA,SAAS,QAAQ,IAAI,kBAAkB,OAAO;;;;;WAK9C,UAAU,WAAW,YAAY,WAAW,YACnD,QAAQ,QAAQ,GAAG,MAAM,OAAO,OAAO,QAAQ,GAAG,KAAK,GAAG,OACxD,cAAc,QAAQ,IAAI,IAAI,cAAc,KAAK,KAAK,sBACrD,MAAM,MAAM,kBAAkB,cAAc,QAAQ,IAAI,MAAM,QAAQ,eAAe,aAAa,IAAI;;;;;;;;;;;;;;YAejG,cAAc,eAAe;WAG1B;aACE;aACA,UAAU;;;;;;;;;;;;YAaf,aAAa,KAAK,QAAQ,GAAG,KAAK;;;;;;;YAQlC,MAAM;;;;;;;iBAQD,kBAAkB;WACxB,oBAAoB;;;;;;;;YASnB,UAAU,KAAK,UAAU,iBAAiB,QAAQ,kBAAkB;;;;;;;;YASpE,cAAc,cAAc,SAAS,cAAc;;;;;;;iBAQ9C,YAAY,UAAU,eAAe,KAAK,OAAO;WACvD,uBAAuB;;;;;WAKvB,UAAU,QAAQ,QAAQ;;WAE1B;;;;;;;;iBASM;WACN,OAAO;;WAEP,QAAQ;WACR,OAAO;;;;;WAKP,SAAS,OAAO,OAAO,cAAc,OAAO;;WAE5C,OAAO,UAAU,eAAe,KAAK,OAAO,sBACnD,SAAS,YAAY,OAClB,OAAO,OACV,OACA,QAAQ,SACR,cAAc,gBAAgB,gBAA8B;;;;;;;;;;;;YAcpD,QAAQ,WAAW,KAAK,MAClC,KAAK,QAEH,OAAO,OAAO,gBAA8B,KAC5C,OAAO,OAAO,OAAO,OAAO,gBAA8B,aAAa;;;;;;;;;;iBAW1D;WACN,MAAM,OAAO,QACnB,KAAK,QAAQ,OAAO,OAAO,gBAA8B,qBAE1D,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8CL,aAAc,qBAAmB,MACtC,MAAI;;WAGC;MAEV,WAAW;;;;;;;;;qBA0CD,OAAK,OAAA;;;;;;;;;;;;;YAoBN,eAAe,MAAM;;;;;;;iBAQhB;;WAEN;WACA,OAAO;;;;;;;;;;;;;;;;;;;qBAeL,wBAAwB;;;WAkBjB,QAAC,SAAS,iBAAiB,OAAO,OAAO,OAAO;;WAE9C,UAAC,OAAO,OAAO,iBAAiB,OAAO,gCAAgC;;WAE1E,OAAC,OAAO,UAAU,OAAO,aAAa;;;;;;;;;;qBAR5C,eAAe"}
1
+ {"version":3,"file":"Specialist.d.ts","names":[],"sources":["../src/Specialist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;cA0BM;;;;;;;iBAQW,WAAW;YAChB,gBAAgB;WACjB,MAAM;;WAEN;;WAEA,SAAS,QAAQ,IAAI,kBAAkB,OAAO;;;;;WAK9C,UAAU,WAAW,YAAY,WAAW,YACnD,QAAQ,QAAQ,GAAG,MAAM,OAAO,OAAO,QAAQ,GAAG,KAAK,GAAG,OACxD,cAAc,QAAQ,IAAI,IAAI,eAAe,KAAK,KAAK,sBACtD,MAAM,MAAM,kBAAkB,cAAc,QAAQ,IAAI,MAAM,QAAQ,eAAe,aAAa,IAAI;;;;;;;;;;;;;;YAejG,cAAc,eAAe;WAG1B;aACE;aACA,UAAU;;;;;;;;;;;;YAaf,aAAa,KAAK,QAAQ,GAAG,KAAK;;;;;;;YAQlC,MAAM;;;;;;;iBAQD,kBAAkB;WACxB,oBAAoB;;;;;;;;YASnB,UAAU,KAAK,UAAU,iBAAiB,QAAQ,kBAAkB;;;;;;;;YASpE,eAAe,cAAc,SAAS,cAAc;;;;;;;iBAQ/C,WAAW,UAAU,eAAe,KAAK,OAAO;WACtD,uBAAuB;;;;;WAKvB,UAAU,QAAQ,QAAQ;;WAE1B;;;;;;;;;iBAUM;WACN,OAAO;;WAEP,QAAQ;WACR,OAAO;;;;;WAKP,SAAS,OAAO,OAAO,cAAc,OAAO;;WAE5C,MAAM,UAAU,eAAe,KAAK,OAAO,sBAClD,SAAS,WAAW,OACjB,OAAO,OACV,OACA,QAAQ,SACR,cAAc,gBAAgB,gBAA8B;;;;;;;;;;;;YAcpD,QAAQ,WAAW,KAAK,MAClC,MAAM,SAEJ,OAAO,OAAO,gBAA8B,KAC5C,OAAO,OAAO,OAAO,OAAO,gBAA8B,aAAa;;;;;;;;;;iBAW1D;WACN,MAAM,OAAO,QACnB,MAAM,SAAS,OAAO,OAAO,gBAA8B,sBAE5D,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8CL,aAAc,qBAAmB,MACtC,MAAI;;WAGC;MAEV,WAAW;;;;;;;;;qBA0CD,OAAK,OAAA;;;;;;;;;;;;;YAoBN,eAAe,MAAM;;;;;;;iBAQhB;;WAEN;WACA,OAAO;;;;;;;;;;;;;;;;;;;qBAeL,wBAAwB;;;WAkBjB,QAAC,SAAS,iBAAiB,OAAO,OAAO,OAAO;;WAE9C,UAAC,OAAO,OAAO,iBAAiB,OAAO,gCAAgC;;WAE1E,OAAC,OAAO,UAAU,OAAO,aAAa;;;;;;;;;;qBAR5C,eAAe"}
@@ -18,7 +18,7 @@ const toStream = (result) => Effect.isEffect(result) ? Stream.unwrap(result) : r
18
18
  *
19
19
  * **Details**
20
20
  *
21
- * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the
21
+ * The specialist's `toLayer` provides its handler, which is usually `turn.run` with the
22
22
  * specialist's own instructions and tools. List the specialist in `Assistant.make`'s
23
23
  * `specialists`, and provide its layer to the assistant's.
24
24
  *
@@ -32,8 +32,8 @@ const toStream = (result) => Effect.isEffect(result) ? Stream.unwrap(result) : r
32
32
  * description: "Researches past campaigns and orders. Give it the question and any context."
33
33
  * })
34
34
  *
35
- * const ResearchLive = Research.toLayer((run) =>
36
- * run.loop({
35
+ * const ResearchLive = Research.toLayer((turn) =>
36
+ * turn.run({
37
37
  * instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
38
38
  * toolkit: ResearchTools
39
39
  * })
@@ -56,7 +56,7 @@ const make = (name, options) => {
56
56
  const context = yield* Effect.context();
57
57
  return { open: Effect.gen(function* () {
58
58
  const handler = Effect.isEffect(build) ? yield* build : build;
59
- return (run) => Stream.provideContext(toStream(handler(run)), context);
59
+ return (turn) => Stream.provideContext(toStream(handler(turn)), context);
60
60
  }).pipe(Effect.provideContext(context), Effect.orDie) };
61
61
  }));
62
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Specialist.js","names":[],"sources":["../src/Specialist.ts"],"sourcesContent":["/**\n * Defines specialists, which are focused agents an assistant hands work to in the background.\n *\n * A specialist has a definition, created with {@link make}, that gives its name and when to use\n * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The\n * {@link Runner} service starts, steers, and stops a chat's specialist runs.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\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 Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantLoop from \"./AssistantLoop.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport type { CurrentChat } from \"./CurrentChat.ts\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport type { RunId } from \"./RunId.ts\"\n\nconst TypeId = \"~effect-assistant/Specialist\"\n\n/**\n * Represents a specialist's definition.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Specialist<Name extends string = string> {\n readonly [TypeId]: typeof TypeId\n readonly name: Name\n /** When to use the specialist. The assistant sees this. */\n readonly description: string\n /** The key of the specialist's handler, as `toLayer` provides it. */\n readonly Handler: Context.Key<SpecialistHandler<Name>, Built>\n /**\n * Provides the specialist's handler. The handler may use the {@link RunServices} and the\n * services its build Effect required (see {@link CheckServices}).\n */\n readonly toLayer: <R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n}\n\n/**\n * Represents the check `toLayer` applies to the services a handler uses.\n *\n * **Details**\n *\n * Every service a handler uses must be one the run (or turn) provides, or one its build Effect\n * required, which the layer provides. A handler that uses any other service gets this type\n * error, which names those services.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type CheckServices<Undeclared> = [Undeclared] extends [never]\n ? unknown\n : {\n readonly \"~undeclaredServices\": {\n readonly error: \"The handler uses services that neither the turn provides nor its build Effect requires\"\n readonly services: Undeclared\n }\n }\n\n/**\n * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.\n *\n * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a\n * missing one is a type error.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolHandlers<R> = Extract<R, Tool.Handler<string>>\n\n/**\n * Represents any specialist.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Any = Specialist<any>\n\n/**\n * Identifies a specialist's handler in a context.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpecialistHandler<Name extends string> {\n readonly _SpecialistHandler: Name\n}\n\n/**\n * Extracts the handlers that these specialists need.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never\n\n/**\n * The services a run provides to its handler, which are its chat, the chat's runner, and the\n * model.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `run.loop`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {\n readonly instructions: string | Instructions.Resolved\n /**\n * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a\n * `Skill.toolkit(...)` for playbooks.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** The model calls allowed per brief. Defaults to 8. */\n readonly maxSteps?: number | undefined\n}\n\n/**\n * Represents one run of a specialist on a brief, as its handler sees it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Run {\n readonly runId: RunId\n /** The chat the run belongs to, which is the chat of the turn that spawned it. */\n readonly chatId: ChatId\n readonly brief: Brief\n /**\n * The run's history, with the brief, what the run did so far, and the messages sent to it since\n * (follow-ups and steering). Read it before every model call, as the loop does.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>\n /** Runs the agent loop over the run's history, with the given instructions and tools. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: LoopOptions<T>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>\n >\n}\n\n/**\n * Represents a specialist's handler, which works on a brief and returns a stream of what the loop\n * did.\n *\n * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the\n * services the Effect needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Handler<R = never, RE = R> = (\n run: Run\n) =>\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n\n/**\n * Represents a specialist's handler as its layer provides it.\n *\n * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns\n * the handler with the layer's services already provided.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Built {\n readonly open: Effect.Effect<\n (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,\n never,\n Scope.Scope | CurrentChat\n >\n}\n\nconst toStream = <R, RE>(\n result:\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)\n\n/**\n * Creates a specialist from its name and a description of when to use it.\n *\n * **Details**\n *\n * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the\n * specialist's own instructions and tools. List the specialist in `Assistant.make`'s\n * `specialists`, and provide its layer to the assistant's.\n *\n * **Example** (Defining a research specialist)\n *\n * ```ts\n * import { Assistant, Specialist } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n *\n * const Research = Specialist.make(\"research\", {\n * description: \"Researches past campaigns and orders. Give it the question and any context.\"\n * })\n *\n * const ResearchLive = Research.toLayer((run) =>\n * run.loop({\n * instructions: \"Answer with the findings and numbers the colleague needs, in a few lines.\",\n * toolkit: ResearchTools\n * })\n * ).pipe(Layer.provide(ResearchToolsLive))\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { specialists: [Research] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(\n * Layer.provide(ResearchLive)\n * )\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Name extends string>(\n name: Name,\n options: {\n /** When to use it. The assistant sees this. */\n readonly description: string\n }\n): Specialist<Name> => {\n const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)\n\n // The signature checks what the handler uses (`CheckServices`); the layer's services, which the\n // build required, are provided to it and to every run's stream, so what's left is the run\n // services.\n function toLayer<R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n function toLayer(\n build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>\n ): Layer.Layer<SpecialistHandler<Name>, never, any> {\n return Layer.effect(\n Handler,\n Effect.gen(function* () {\n const context = yield* Effect.context()\n\n return {\n // The build runs per chat activation, in the chat; here it only keeps the layer's\n // services for it.\n open: Effect.gen(function* () {\n const handler = Effect.isEffect(build) ? yield* build : build\n\n return (run: Run) => Stream.provideContext(toStream(handler(run)), context)\n }).pipe(Effect.provideContext(context), Effect.orDie)\n }\n })\n )\n }\n\n return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }\n}\n\n/**\n * Schema for the brief the assistant gives a specialist.\n *\n * Only `goal` is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Brief = Schema.Struct({\n goal: Schema.String.annotate({ description: \"What to find out or do.\" }),\n context: Schema.optional(Schema.String).annotate({\n description: \"What it needs to know. It can't see this chat.\"\n }),\n successCriteria: Schema.optional(Schema.String).annotate({ description: \"When it is done.\" }),\n report: Schema.optional(Schema.String).annotate({\n description: \"What to put in the answer, and how.\"\n }),\n limits: Schema.optional(Schema.String).annotate({\n description: \"Hard limits for this job, like read-only or a deadline.\"\n })\n})\n\n/**\n * Represents the brief the assistant gives a specialist.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Brief = typeof Brief.Type\n\n/**\n * Represents a request to start a specialist run in the runner's chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpawnRequest {\n /** The specialist's name. */\n readonly specialist: string\n readonly brief: Brief\n}\n\n/**\n * Represents why a run request can't be carried out.\n *\n * **Details**\n *\n * The `reason` says which request failed: `\"UnknownSpecialist\"` when `spawn` names no specialist\n * of the assistant, `\"RunNotFound\"` when `message` names a run that is gone, and `\"NotRunning\"`\n * when `stop` names a run that isn't running. The `message` is written for the model.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SpecialistError extends Schema.Error<SpecialistError>(\"effect-assistant/Specialist/SpecialistError\")({\n _tag: Schema.tag(\"SpecialistError\"),\n reason: Schema.Literals([\"UnknownSpecialist\", \"RunNotFound\", \"NotRunning\"]),\n message: Schema.String\n}) {}\n\n/**\n * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.\n *\n * The engine provides it to every turn. The specialist tools use it, and a handler can too.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Runner extends Context.Service<\n Runner,\n {\n /** Starts a run and returns its id. */\n readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>\n /** Steers a running run, or continues a finished one. */\n readonly message: (runId: RunId, text: string) => Effect.Effect<\"steered\" | \"continued\", SpecialistError>\n /** Interrupts a running run. */\n readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>\n }\n>()(\"effect-assistant/Specialist/Runner\") {}\n"],"mappings":";;;;;;;;;;;;;AA0BA,MAAM,SAAS;AAwKf,MAAM,YACJ,WAGyD,OAAO,SAAS,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC7G,MAAa,QACX,MACA,YAIqB;CACrB,MAAM,UAAU,QAAQ,QAAwC,+BAA+B,MAAM;CASrG,SAAS,QACP,OACkD;EAClD,OAAO,MAAM,OACX,SACA,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ;GAEtC,OAAO,EAGL,MAAM,OAAO,IAAI,aAAa;IAC5B,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,OAAO,QAAQ;IAExD,QAAQ,QAAa,OAAO,eAAe,SAAS,QAAQ,GAAG,CAAC,GAAG,OAAO;GAC5E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,GAAG,OAAO,KAAK,EACtD;EACF,CAAC,CACH;CACF;CAEA,OAAO;GAAG,SAAS;EAAQ;EAAM,aAAa,QAAQ;EAAa;EAAS;CAAQ;AACtF;;;;;;;;;AAUA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,0BAA0B,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC/C,aAAa,iDACf,CAAC;CACD,iBAAiB,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE,aAAa,mBAAmB,CAAC;CAC5F,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,sCACf,CAAC;CACD,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,0DACf,CAAC;AACH,CAAC;;;;;;;;;;;;;AAkCD,IAAa,kBAAb,cAAqC,OAAO,MAAuB,6CAA6C,CAAC,CAAC;CAChH,MAAM,OAAO,IAAI,iBAAiB;CAClC,QAAQ,OAAO,SAAS;EAAC;EAAqB;EAAe;CAAY,CAAC;CAC1E,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;AAUJ,IAAa,SAAb,cAA4B,QAAQ,QAUlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"Specialist.js","names":[],"sources":["../src/Specialist.ts"],"sourcesContent":["/**\n * Defines specialists, which are focused agents an assistant hands work to in the background.\n *\n * A specialist has a definition, created with {@link make}, that gives its name and when to use\n * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The\n * {@link Runner} service starts, steers, and stops a chat's specialist runs.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\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 Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantLoop from \"./AssistantLoop.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport type { CurrentChat } from \"./CurrentChat.ts\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport type { RunId } from \"./RunId.ts\"\n\nconst TypeId = \"~effect-assistant/Specialist\"\n\n/**\n * Represents a specialist's definition.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Specialist<Name extends string = string> {\n readonly [TypeId]: typeof TypeId\n readonly name: Name\n /** When to use the specialist. The assistant sees this. */\n readonly description: string\n /** The key of the specialist's handler, as `toLayer` provides it. */\n readonly Handler: Context.Key<SpecialistHandler<Name>, Built>\n /**\n * Provides the specialist's handler. The handler may use the {@link TurnServices} and the\n * services its build Effect required (see {@link CheckServices}).\n */\n readonly toLayer: <R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>\n ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n}\n\n/**\n * Represents the check `toLayer` applies to the services a handler uses.\n *\n * **Details**\n *\n * Every service a handler uses must be one the run (or turn) provides, or one its build Effect\n * required, which the layer provides. A handler that uses any other service gets this type\n * error, which names those services.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type CheckServices<Undeclared> = [Undeclared] extends [never]\n ? unknown\n : {\n readonly \"~undeclaredServices\": {\n readonly error: \"The handler uses services that neither the turn provides nor its build Effect requires\"\n readonly services: Undeclared\n }\n }\n\n/**\n * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.\n *\n * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a\n * missing one is a type error.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolHandlers<R> = Extract<R, Tool.Handler<string>>\n\n/**\n * Represents any specialist.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Any = Specialist<any>\n\n/**\n * Identifies a specialist's handler in a context.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpecialistHandler<Name extends string> {\n readonly _SpecialistHandler: Name\n}\n\n/**\n * Extracts the handlers that these specialists need.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never\n\n/**\n * The services a turn provides to its handler, which are its chat, the chat's runner, and the\n * model.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type TurnServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `turn.run`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>> {\n readonly instructions: string | Instructions.Resolved\n /**\n * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a\n * `Skill.toolkit(...)` for playbooks.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** The model calls allowed per brief. Defaults to 8. */\n readonly maxSteps?: number | undefined\n}\n\n/**\n * Represents one turn of a specialist's run, as its handler sees it. The first turn works on the\n * brief, and each later one on the messages sent to the run since (follow-ups and steering).\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Turn {\n readonly runId: RunId\n /** The chat the run belongs to, which is the chat of the turn that spawned it. */\n readonly chatId: ChatId\n readonly brief: Brief\n /**\n * The run's history, with the brief, what the run did so far, and the messages sent to it since\n * (follow-ups and steering). Read it before every model call, as the loop does.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>\n /** Runs the agent loop over the run's history, with the given instructions and tools. */\n readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: RunOptions<T>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>\n >\n}\n\n/**\n * Represents a specialist's handler, which works on a brief and returns a stream of what the loop\n * did.\n *\n * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the\n * services the Effect needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Handler<R = never, RE = R> = (\n turn: Turn\n) =>\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n\n/**\n * Represents a specialist's handler as its layer provides it.\n *\n * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns\n * the handler with the layer's services already provided.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Built {\n readonly open: Effect.Effect<\n (turn: Turn) => Stream.Stream<AssistantLoop.Event, unknown, TurnServices>,\n never,\n Scope.Scope | CurrentChat\n >\n}\n\nconst toStream = <R, RE>(\n result:\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)\n\n/**\n * Creates a specialist from its name and a description of when to use it.\n *\n * **Details**\n *\n * The specialist's `toLayer` provides its handler, which is usually `turn.run` with the\n * specialist's own instructions and tools. List the specialist in `Assistant.make`'s\n * `specialists`, and provide its layer to the assistant's.\n *\n * **Example** (Defining a research specialist)\n *\n * ```ts\n * import { Assistant, Specialist } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n *\n * const Research = Specialist.make(\"research\", {\n * description: \"Researches past campaigns and orders. Give it the question and any context.\"\n * })\n *\n * const ResearchLive = Research.toLayer((turn) =>\n * turn.run({\n * instructions: \"Answer with the findings and numbers the colleague needs, in a few lines.\",\n * toolkit: ResearchTools\n * })\n * ).pipe(Layer.provide(ResearchToolsLive))\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { specialists: [Research] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(\n * Layer.provide(ResearchLive)\n * )\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Name extends string>(\n name: Name,\n options: {\n /** When to use it. The assistant sees this. */\n readonly description: string\n }\n): Specialist<Name> => {\n const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)\n\n // The signature checks what the handler uses (`CheckServices`); the layer's services, which the\n // build required, are provided to it and to every run's stream, so what's left is the run\n // services.\n function toLayer<R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>\n ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n function toLayer(\n build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>\n ): Layer.Layer<SpecialistHandler<Name>, never, any> {\n return Layer.effect(\n Handler,\n Effect.gen(function* () {\n const context = yield* Effect.context()\n\n return {\n // The build runs per chat activation, in the chat; here it only keeps the layer's\n // services for it.\n open: Effect.gen(function* () {\n const handler = Effect.isEffect(build) ? yield* build : build\n\n return (turn: Turn) => Stream.provideContext(toStream(handler(turn)), context)\n }).pipe(Effect.provideContext(context), Effect.orDie)\n }\n })\n )\n }\n\n return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }\n}\n\n/**\n * Schema for the brief the assistant gives a specialist.\n *\n * Only `goal` is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Brief = Schema.Struct({\n goal: Schema.String.annotate({ description: \"What to find out or do.\" }),\n context: Schema.optional(Schema.String).annotate({\n description: \"What it needs to know. It can't see this chat.\"\n }),\n successCriteria: Schema.optional(Schema.String).annotate({ description: \"When it is done.\" }),\n report: Schema.optional(Schema.String).annotate({\n description: \"What to put in the answer, and how.\"\n }),\n limits: Schema.optional(Schema.String).annotate({\n description: \"Hard limits for this job, like read-only or a deadline.\"\n })\n})\n\n/**\n * Represents the brief the assistant gives a specialist.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Brief = typeof Brief.Type\n\n/**\n * Represents a request to start a specialist run in the runner's chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpawnRequest {\n /** The specialist's name. */\n readonly specialist: string\n readonly brief: Brief\n}\n\n/**\n * Represents why a run request can't be carried out.\n *\n * **Details**\n *\n * The `reason` says which request failed: `\"UnknownSpecialist\"` when `spawn` names no specialist\n * of the assistant, `\"RunNotFound\"` when `message` names a run that is gone, and `\"NotRunning\"`\n * when `stop` names a run that isn't running. The `message` is written for the model.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SpecialistError extends Schema.Error<SpecialistError>(\"effect-assistant/Specialist/SpecialistError\")({\n _tag: Schema.tag(\"SpecialistError\"),\n reason: Schema.Literals([\"UnknownSpecialist\", \"RunNotFound\", \"NotRunning\"]),\n message: Schema.String\n}) {}\n\n/**\n * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.\n *\n * The engine provides it to every turn. The specialist tools use it, and a handler can too.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Runner extends Context.Service<\n Runner,\n {\n /** Starts a run and returns its id. */\n readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>\n /** Steers a running run, or continues a finished one. */\n readonly message: (runId: RunId, text: string) => Effect.Effect<\"steered\" | \"continued\", SpecialistError>\n /** Interrupts a running run. */\n readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>\n }\n>()(\"effect-assistant/Specialist/Runner\") {}\n"],"mappings":";;;;;;;;;;;;;AA0BA,MAAM,SAAS;AAyKf,MAAM,YACJ,WAGyD,OAAO,SAAS,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC7G,MAAa,QACX,MACA,YAIqB;CACrB,MAAM,UAAU,QAAQ,QAAwC,+BAA+B,MAAM;CASrG,SAAS,QACP,OACkD;EAClD,OAAO,MAAM,OACX,SACA,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ;GAEtC,OAAO,EAGL,MAAM,OAAO,IAAI,aAAa;IAC5B,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,OAAO,QAAQ;IAExD,QAAQ,SAAe,OAAO,eAAe,SAAS,QAAQ,IAAI,CAAC,GAAG,OAAO;GAC/E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,GAAG,OAAO,KAAK,EACtD;EACF,CAAC,CACH;CACF;CAEA,OAAO;GAAG,SAAS;EAAQ;EAAM,aAAa,QAAQ;EAAa;EAAS;CAAQ;AACtF;;;;;;;;;AAUA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,0BAA0B,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC/C,aAAa,iDACf,CAAC;CACD,iBAAiB,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE,aAAa,mBAAmB,CAAC;CAC5F,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,sCACf,CAAC;CACD,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,0DACf,CAAC;AACH,CAAC;;;;;;;;;;;;;AAkCD,IAAa,kBAAb,cAAqC,OAAO,MAAuB,6CAA6C,CAAC,CAAC;CAChH,MAAM,OAAO,IAAI,iBAAiB;CAClC,QAAQ,OAAO,SAAS;EAAC;EAAqB;EAAe;CAAY,CAAC;CAC1E,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;AAUJ,IAAa,SAAb,cAA4B,QAAQ,QAUlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC"}
@@ -164,11 +164,11 @@ const owesDelivery = (entries, throughSequence) => {
164
164
  const latest = entries.findLast(({ event, sequence }) => sequence <= throughSequence && isInput$1(event) && isUserInput(event));
165
165
  if (latest === void 0) return false;
166
166
  const after = entries.flatMap(({ sequence, event }) => sequence > latest.sequence && isLibraryEvent(event) ? [event] : []);
167
- const messages = after.flatMap((event) => Predicate.isTagged(event, "AssistantMessage") ? [event] : []);
167
+ const last = after.findLast((event) => Predicate.isTagged(event, "AssistantMessage"));
168
168
  const progress = progressMessages(entries);
169
169
  const started = after.flatMap((event) => Predicate.isTagged(event, "SpecialistStarted") ? [event.runId] : []);
170
170
  const finished = new Set(entries.flatMap(({ event }) => isLibraryEvent(event) && Predicate.isTagged(event, "SpecialistFinished") ? [event.runId] : []));
171
- return messages.length > 0 && messages.every((message) => message.block === void 0 && progress.has(message.id)) && !after.some((event) => Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")) && started.every((runId) => finished.has(runId));
171
+ return last !== void 0 && last.block === void 0 && progress.has(last.id) && !after.some((event) => Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")) && started.every((runId) => finished.has(runId));
172
172
  };
173
173
  /** @internal */
174
174
  const unreadAfter = (entries, throughSequence) => entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event));
@@ -1 +1 @@
1
- {"version":3,"file":"history.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.textWithAttachments","AssistantEvent.isInput","AssistantEvent.isAppInput","AssistantEvent.isTurnEnd","AssistantEvent.isWaking","AssistantEvent.isUserInput","Uuid.fromKey"],"sources":["../../src/internal/history.ts"],"sourcesContent":["import * as Prompt from \"effect/ai/Prompt\"\nimport * as Response from \"effect/ai/Response\"\nimport * as Match from \"effect/Match\"\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 { toJson, toJsonText } from \"./schema.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport const userText = (text: string): Prompt.UserMessage =>\n Prompt.userMessage({ content: [Prompt.textPart({ text })] })\n\n/** @internal */\nexport const toolMessage = (result: AssistantEvent.ToolResult): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: result.callId,\n name: result.tool,\n isFailure: result.outcome === \"failure\",\n result: result.result,\n providerExecuted: false\n })\n ]\n })\n\n// A call with no result is one a crash cut off.\n/** @internal */\nexport const unansweredCalls = (history: ReadonlyArray<Prompt.Message>): ReadonlyArray<AssistantEvent.ToolCallPart> => {\n const answered = new Set(\n history.flatMap((message) =>\n message.role === \"tool\" ? message.content.flatMap((part) => (part.type === \"tool-result\" ? [part.id] : [])) : []\n )\n )\n\n return history.flatMap((message) =>\n message.role === \"assistant\"\n ? message.content.flatMap((part) =>\n part.type === \"tool-call\" && !part.providerExecuted && !answered.has(part.id)\n ? [\n Response.makePart(\"tool-call\", {\n id: part.id,\n name: part.name,\n params: toJson(part.params),\n providerExecuted: false,\n metadata: part.options\n })\n ]\n : []\n )\n : []\n )\n}\n\n/** @internal */\nexport const notRunOutput = \"Not run: the turn ended before this call ran.\"\n\n/** @internal */\nexport const mayHaveRunOutput =\n \"Stopped: the turn ended while this call ran. It may have done its work; check before doing it again.\"\n\nconst ago = (millis: number) => {\n const seconds = Math.max(0, Math.round(millis / 1000))\n\n return seconds < 90 ? `${seconds} seconds` : `${Math.round(seconds / 60)} minutes`\n}\n\nconst finishedText = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n index: number,\n finished: AssistantEvent.SpecialistFinished\n) => {\n const at = entries[index]!\n\n const isStart = ({ event }: AssistantEvent.AnyEntry) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"SpecialistStarted\") &&\n event.runId === finished.runId\n\n const from = entries.slice(0, index).findLastIndex(isStart)\n const started = from === -1 ? undefined : entries[from]\n const since = started === undefined ? [] : entries.slice(from + 1, index)\n const wrote = since.some(({ event }) => Predicate.isTagged(event, \"UserMessage\"))\n\n const lines = [\n \"A report from a specialist you started. The user hasn't seen it.\",\n started === undefined\n ? `You started it for: ${finished.goal}`\n : `You started it ${ago(at.at - started.at)} ago for: ${finished.goal}`,\n wrote ? \"The user has written since you started it.\" : \"The user hasn't written since you started it.\",\n Match.value(finished.status).pipe(\n Match.when(\"answered\", () => `Report:\\n${finished.report ?? \"\"}`),\n Match.when(\"failed\", () => `It failed: ${finished.error ?? \"\"}`),\n Match.when(\"cut_off\", () => \"A restart cut it off before it finished. Spawn it again if it's still needed.\"),\n Match.when(\"stopped\", () => \"You stopped it.\"),\n Match.exhaustive\n )\n ]\n\n return `<specialist name=\"${finished.specialist}\" run=\"${finished.runId}\" status=\"${finished.status}\">\\n${lines.join(\n \"\\n\"\n )}\\n</specialist>`\n}\n\nconst answerText = (answer: AssistantEvent.BlockAnswer) =>\n `<answer message=\"${answer.messageId}\" kind=\"${answer.kind}\">\\nThe user answered with the block on your message ${answer.messageId}.\\n${toJsonText(answer.value)}\\n</answer>`\n\nconst resultText = (result: AssistantEvent.ToolResult) =>\n `<result tool=\"${result.tool}\" call=\"${result.callId}\" outcome=\"${result.outcome}\">\\nThe result of a call from earlier in this chat.\\n${toJsonText(result.result)}\\n</result>`\n\n/** @internal */\nexport const awaitingApprovalOutput =\n \"Waiting for the user's approval: they see this call and approve or reject it. Its outcome arrives later in this chat; don't call it again meanwhile.\"\n\nconst awaitingText = (request: AssistantEvent.ApprovalRequested) =>\n `<approval tool=\"${request.tool}\" call=\"${request.callId}\" status=\"pending\">\\nYour call to ${request.tool} from earlier in this chat waits for the user's approval.\\n${toJsonText(request.params)}\\n</approval>`\n\nconst approvedText = (result: AssistantEvent.ToolResult) =>\n `<result tool=\"${result.tool}\" call=\"${result.callId}\" outcome=\"${result.outcome}\">\\nThe user approved your call to ${result.tool} from earlier in this chat, and it ran.\\n${toJsonText(result.result)}\\n</result>`\n\nconst rejectedText = (decision: AssistantEvent.ApprovalDecision, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${decision.callId}\" decision=\"rejected\">\\nThe user rejected your call to ${tool}; it didn't run.${decision.reason === undefined ? \"\" : `\\nTheir reason: ${decision.reason}`}\\n</approval>`\n\nconst expiredText = (callId: string, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${callId}\" status=\"expired\">\\nThe user moved on without approving your call to ${tool}; it didn't run.\\n</approval>`\n\nconst awaitingMessage = (request: AssistantEvent.ApprovalRequested): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: request.callId,\n name: request.tool,\n isFailure: false,\n result: awaitingApprovalOutput,\n providerExecuted: false\n })\n ]\n })\n\nconst isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>\n Predicate.hasProperty(item, \"sequence\")\n\n// In the order the model saw the entries, not the order they were stored. A message a history view put in stands\n// where it is, right after the entry before it, and waits like an input for a call's result or for the model's next\n// read; the app's own events reach the model only as such messages.\n/** @internal */\nexport const fromEntries = (\n items: ReadonlyArray<AssistantEvent.AnyEntry | Prompt.Message>\n): ReadonlyArray<Prompt.Message> => {\n const entries = items.filter(isEntry)\n const history: Array<Prompt.Message> = []\n const held: Array<Prompt.Message> = []\n const open = new Set<string>()\n const called = new Set<string>()\n // Calls that waited for approval: the placeholder answered them, so what came of them later is an input.\n const requests = new Map<string, AssistantEvent.ApprovalRequested>()\n const toolOf = (callId: string) => requests.get(callId)?.tool ?? \"a tool\"\n\n const flushIfClosed = () => {\n if (open.size === 0) history.push(...held.splice(0))\n }\n\n const nextSeen = (from: number) => {\n const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, \"ModelOutput\"))?.event\n\n return next !== undefined && AssistantEvent.isLibraryEvent(next) && Predicate.isTagged(next, \"ModelOutput\")\n ? next.seenThroughSequence\n : undefined\n }\n\n // `index` is the last entry at or before the item, and `sequence` where the item stands.\n const input = (index: number, sequence: number, item: Prompt.Message) => {\n const seen = nextSeen(index)\n\n if (open.size > 0 || (seen !== undefined && seen < sequence)) held.push(item)\n else history.push(item)\n }\n\n let index = -1\n\n for (const item of items) {\n if (!isEntry(item)) {\n input(index, (entries[index]?.sequence ?? 0) + 0.5, item)\n\n continue\n }\n\n index += 1\n\n const { event, sequence } = item\n const at = index\n\n if (!AssistantEvent.isLibraryEvent(event)) continue\n\n Match.value(event).pipe(\n Match.tagsExhaustive({\n UserMessage: (message) =>\n input(at, sequence, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),\n BlockAnswer: (answer) => input(at, sequence, userText(answerText(answer))),\n ModelOutput: (output) => {\n history.push(...Prompt.fromResponseParts(output.parts).content)\n\n for (const part of output.parts) {\n if (part.type === \"tool-call\") {\n open.add(part.id)\n called.add(part.id)\n }\n }\n\n flushIfClosed()\n },\n ToolResult: (result) => {\n if (requests.has(result.callId)) return input(at, sequence, userText(approvedText(result)))\n\n // A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its\n // call right before it.\n if (!called.has(result.callId)) return input(at, sequence, userText(resultText(result)))\n\n history.push(toolMessage(result))\n open.delete(result.callId)\n flushIfClosed()\n },\n AssistantMessage: () => undefined,\n ToolStarted: () => undefined,\n TurnStarted: () => undefined,\n TurnCompleted: () => undefined,\n TurnFailed: () => undefined,\n SpecialistFinished: (finished) => input(at, sequence, userText(finishedText(entries, at, finished))),\n // Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.\n SpecialistStarted: () => undefined,\n BlocksClosed: () => undefined,\n // The call's result, for the provider's pairing; what the user decides comes later.\n ApprovalRequested: (request) => {\n requests.set(request.callId, request)\n\n if (!called.has(request.callId)) return input(at, sequence, userText(awaitingText(request)))\n\n history.push(awaitingMessage(request))\n open.delete(request.callId)\n flushIfClosed()\n },\n // An approved call's result says so.\n ApprovalDecision: (decision) =>\n decision.decision === \"rejected\"\n ? input(at, sequence, userText(rejectedText(decision, toolOf(decision.callId))))\n : undefined,\n ApprovalsExpired: (expired) =>\n expired.callIds.forEach((callId) => input(at, sequence, userText(expiredText(callId, toolOf(callId))))),\n ChatDeleted: () => undefined\n })\n )\n }\n\n return [...history, ...held]\n}\n\n/** @internal */\nexport const isInput = (event: { readonly _tag: string }): boolean =>\n AssistantEvent.isInput(event) || AssistantEvent.isAppInput(event)\n\n/** @internal */\nexport const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {\n const last = entries.findLast(({ event }) => AssistantEvent.isTurnEnd(event))?.event\n\n return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.throughSequence : 0\n}\n\n// A run the assistant stopped isn't an input here: it wakes nothing.\n/** @internal */\nexport const unhandledInputs = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<{ readonly _tag: string }> => {\n const seen = handledThrough(entries)\n\n return entries.flatMap(({ event, sequence }) =>\n sequence > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event))\n ? [event]\n : []\n )\n}\n\n/** @internal */\nexport const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): boolean =>\n unhandledInputs(entries).length > 0\n\n// A failure the user was shown counts as a reply, and so does a call they're asked to approve.\n/** @internal */\nexport const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n return (\n latest !== undefined &&\n !entries.some(\n ({ event, sequence }) =>\n sequence > latest.sequence &&\n (Predicate.isTagged(event, \"AssistantMessage\") ||\n Predicate.isTagged(event, \"TurnFailed\") ||\n Predicate.isTagged(event, \"ApprovalRequested\"))\n )\n )\n}\n\nconst sentToWaitOn = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(false) }))\n\n// Messages sent with `wait: false` are progress while the model kept working, like \"On it\".\nconst progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlySet<string> =>\n new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ModelOutput\")\n ? event.parts.flatMap((part) =>\n part.type === \"tool-call\" && part.name === \"send_user_message\" && Option.isSome(sentToWaitOn(part.params))\n ? [Uuid.fromKey(`message:${part.id}`)]\n : []\n )\n : []\n )\n )\n\n// The model said it was on it and stopped without the result: only progress messages since the user's latest input.\n// A specialist still working doesn't count, since its report is what comes next.\n/** @internal */\nexport const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n if (latest === undefined) return false\n\n const after = entries.flatMap(({ sequence, event }) =>\n sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []\n )\n\n const messages = after.flatMap((event) => (Predicate.isTagged(event, \"AssistantMessage\") ? [event] : []))\n const progress = progressMessages(entries)\n const started = after.flatMap((event) => (Predicate.isTagged(event, \"SpecialistStarted\") ? [event.runId] : []))\n\n const finished = new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"SpecialistFinished\") ? [event.runId] : []\n )\n )\n\n // A call waiting for the user's approval is what comes next, like a specialist's report.\n return (\n messages.length > 0 &&\n messages.every((message) => message.block === undefined && progress.has(message.id)) &&\n !after.some((event) => Predicate.isTagged(event, \"TurnFailed\") || Predicate.isTagged(event, \"ApprovalRequested\")) &&\n started.every((runId) => finished.has(runId))\n )\n}\n\n/** @internal */\nexport const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean =>\n entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event))\n"],"mappings":"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"}
1
+ {"version":3,"file":"history.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.textWithAttachments","AssistantEvent.isInput","AssistantEvent.isAppInput","AssistantEvent.isTurnEnd","AssistantEvent.isWaking","AssistantEvent.isUserInput","Uuid.fromKey"],"sources":["../../src/internal/history.ts"],"sourcesContent":["import * as Prompt from \"effect/ai/Prompt\"\nimport * as Response from \"effect/ai/Response\"\nimport * as Match from \"effect/Match\"\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 { toJson, toJsonText } from \"./schema.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport const userText = (text: string): Prompt.UserMessage =>\n Prompt.userMessage({ content: [Prompt.textPart({ text })] })\n\n/** @internal */\nexport const toolMessage = (result: AssistantEvent.ToolResult): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: result.callId,\n name: result.tool,\n isFailure: result.outcome === \"failure\",\n result: result.result,\n providerExecuted: false\n })\n ]\n })\n\n// A call with no result is one a crash cut off.\n/** @internal */\nexport const unansweredCalls = (history: ReadonlyArray<Prompt.Message>): ReadonlyArray<AssistantEvent.ToolCallPart> => {\n const answered = new Set(\n history.flatMap((message) =>\n message.role === \"tool\" ? message.content.flatMap((part) => (part.type === \"tool-result\" ? [part.id] : [])) : []\n )\n )\n\n return history.flatMap((message) =>\n message.role === \"assistant\"\n ? message.content.flatMap((part) =>\n part.type === \"tool-call\" && !part.providerExecuted && !answered.has(part.id)\n ? [\n Response.makePart(\"tool-call\", {\n id: part.id,\n name: part.name,\n params: toJson(part.params),\n providerExecuted: false,\n metadata: part.options\n })\n ]\n : []\n )\n : []\n )\n}\n\n/** @internal */\nexport const notRunOutput = \"Not run: the turn ended before this call ran.\"\n\n/** @internal */\nexport const mayHaveRunOutput =\n \"Stopped: the turn ended while this call ran. It may have done its work; check before doing it again.\"\n\nconst ago = (millis: number) => {\n const seconds = Math.max(0, Math.round(millis / 1000))\n\n return seconds < 90 ? `${seconds} seconds` : `${Math.round(seconds / 60)} minutes`\n}\n\nconst finishedText = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n index: number,\n finished: AssistantEvent.SpecialistFinished\n) => {\n const at = entries[index]!\n\n const isStart = ({ event }: AssistantEvent.AnyEntry) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"SpecialistStarted\") &&\n event.runId === finished.runId\n\n const from = entries.slice(0, index).findLastIndex(isStart)\n const started = from === -1 ? undefined : entries[from]\n const since = started === undefined ? [] : entries.slice(from + 1, index)\n const wrote = since.some(({ event }) => Predicate.isTagged(event, \"UserMessage\"))\n\n const lines = [\n \"A report from a specialist you started. The user hasn't seen it.\",\n started === undefined\n ? `You started it for: ${finished.goal}`\n : `You started it ${ago(at.at - started.at)} ago for: ${finished.goal}`,\n wrote ? \"The user has written since you started it.\" : \"The user hasn't written since you started it.\",\n Match.value(finished.status).pipe(\n Match.when(\"answered\", () => `Report:\\n${finished.report ?? \"\"}`),\n Match.when(\"failed\", () => `It failed: ${finished.error ?? \"\"}`),\n Match.when(\"cut_off\", () => \"A restart cut it off before it finished. Spawn it again if it's still needed.\"),\n Match.when(\"stopped\", () => \"You stopped it.\"),\n Match.exhaustive\n )\n ]\n\n return `<specialist name=\"${finished.specialist}\" run=\"${finished.runId}\" status=\"${finished.status}\">\\n${lines.join(\n \"\\n\"\n )}\\n</specialist>`\n}\n\nconst answerText = (answer: AssistantEvent.BlockAnswer) =>\n `<answer message=\"${answer.messageId}\" kind=\"${answer.kind}\">\\nThe user answered with the block on your message ${answer.messageId}.\\n${toJsonText(answer.value)}\\n</answer>`\n\nconst resultText = (result: AssistantEvent.ToolResult) =>\n `<result tool=\"${result.tool}\" call=\"${result.callId}\" outcome=\"${result.outcome}\">\\nThe result of a call from earlier in this chat.\\n${toJsonText(result.result)}\\n</result>`\n\n/** @internal */\nexport const awaitingApprovalOutput =\n \"Waiting for the user's approval: they see this call and approve or reject it. Its outcome arrives later in this chat; don't call it again meanwhile.\"\n\nconst awaitingText = (request: AssistantEvent.ApprovalRequested) =>\n `<approval tool=\"${request.tool}\" call=\"${request.callId}\" status=\"pending\">\\nYour call to ${request.tool} from earlier in this chat waits for the user's approval.\\n${toJsonText(request.params)}\\n</approval>`\n\nconst approvedText = (result: AssistantEvent.ToolResult) =>\n `<result tool=\"${result.tool}\" call=\"${result.callId}\" outcome=\"${result.outcome}\">\\nThe user approved your call to ${result.tool} from earlier in this chat, and it ran.\\n${toJsonText(result.result)}\\n</result>`\n\nconst rejectedText = (decision: AssistantEvent.ApprovalDecision, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${decision.callId}\" decision=\"rejected\">\\nThe user rejected your call to ${tool}; it didn't run.${decision.reason === undefined ? \"\" : `\\nTheir reason: ${decision.reason}`}\\n</approval>`\n\nconst expiredText = (callId: string, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${callId}\" status=\"expired\">\\nThe user moved on without approving your call to ${tool}; it didn't run.\\n</approval>`\n\nconst awaitingMessage = (request: AssistantEvent.ApprovalRequested): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: request.callId,\n name: request.tool,\n isFailure: false,\n result: awaitingApprovalOutput,\n providerExecuted: false\n })\n ]\n })\n\nconst isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>\n Predicate.hasProperty(item, \"sequence\")\n\n// In the order the model saw the entries, not the order they were stored. A message a history view put in stands\n// where it is, right after the entry before it, and waits like an input for a call's result or for the model's next\n// read; the app's own events reach the model only as such messages.\n/** @internal */\nexport const fromEntries = (\n items: ReadonlyArray<AssistantEvent.AnyEntry | Prompt.Message>\n): ReadonlyArray<Prompt.Message> => {\n const entries = items.filter(isEntry)\n const history: Array<Prompt.Message> = []\n const held: Array<Prompt.Message> = []\n const open = new Set<string>()\n const called = new Set<string>()\n // Calls that waited for approval: the placeholder answered them, so what came of them later is an input.\n const requests = new Map<string, AssistantEvent.ApprovalRequested>()\n const toolOf = (callId: string) => requests.get(callId)?.tool ?? \"a tool\"\n\n const flushIfClosed = () => {\n if (open.size === 0) history.push(...held.splice(0))\n }\n\n const nextSeen = (from: number) => {\n const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, \"ModelOutput\"))?.event\n\n return next !== undefined && AssistantEvent.isLibraryEvent(next) && Predicate.isTagged(next, \"ModelOutput\")\n ? next.seenThroughSequence\n : undefined\n }\n\n // `index` is the last entry at or before the item, and `sequence` where the item stands.\n const input = (index: number, sequence: number, item: Prompt.Message) => {\n const seen = nextSeen(index)\n\n if (open.size > 0 || (seen !== undefined && seen < sequence)) held.push(item)\n else history.push(item)\n }\n\n let index = -1\n\n for (const item of items) {\n if (!isEntry(item)) {\n input(index, (entries[index]?.sequence ?? 0) + 0.5, item)\n\n continue\n }\n\n index += 1\n\n const { event, sequence } = item\n const at = index\n\n if (!AssistantEvent.isLibraryEvent(event)) continue\n\n Match.value(event).pipe(\n Match.tagsExhaustive({\n UserMessage: (message) =>\n input(at, sequence, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),\n BlockAnswer: (answer) => input(at, sequence, userText(answerText(answer))),\n ModelOutput: (output) => {\n history.push(...Prompt.fromResponseParts(output.parts).content)\n\n for (const part of output.parts) {\n if (part.type === \"tool-call\") {\n open.add(part.id)\n called.add(part.id)\n }\n }\n\n flushIfClosed()\n },\n ToolResult: (result) => {\n if (requests.has(result.callId)) return input(at, sequence, userText(approvedText(result)))\n\n // A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its\n // call right before it.\n if (!called.has(result.callId)) return input(at, sequence, userText(resultText(result)))\n\n history.push(toolMessage(result))\n open.delete(result.callId)\n flushIfClosed()\n },\n AssistantMessage: () => undefined,\n ToolStarted: () => undefined,\n TurnStarted: () => undefined,\n TurnCompleted: () => undefined,\n TurnFailed: () => undefined,\n SpecialistFinished: (finished) => input(at, sequence, userText(finishedText(entries, at, finished))),\n // Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.\n SpecialistStarted: () => undefined,\n BlocksClosed: () => undefined,\n // The call's result, for the provider's pairing; what the user decides comes later.\n ApprovalRequested: (request) => {\n requests.set(request.callId, request)\n\n if (!called.has(request.callId)) return input(at, sequence, userText(awaitingText(request)))\n\n history.push(awaitingMessage(request))\n open.delete(request.callId)\n flushIfClosed()\n },\n // An approved call's result says so.\n ApprovalDecision: (decision) =>\n decision.decision === \"rejected\"\n ? input(at, sequence, userText(rejectedText(decision, toolOf(decision.callId))))\n : undefined,\n ApprovalsExpired: (expired) =>\n expired.callIds.forEach((callId) => input(at, sequence, userText(expiredText(callId, toolOf(callId))))),\n ChatDeleted: () => undefined\n })\n )\n }\n\n return [...history, ...held]\n}\n\n/** @internal */\nexport const isInput = (event: { readonly _tag: string }): boolean =>\n AssistantEvent.isInput(event) || AssistantEvent.isAppInput(event)\n\n/** @internal */\nexport const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {\n const last = entries.findLast(({ event }) => AssistantEvent.isTurnEnd(event))?.event\n\n return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.throughSequence : 0\n}\n\n// A run the assistant stopped isn't an input here: it wakes nothing.\n/** @internal */\nexport const unhandledInputs = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<{ readonly _tag: string }> => {\n const seen = handledThrough(entries)\n\n return entries.flatMap(({ event, sequence }) =>\n sequence > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event))\n ? [event]\n : []\n )\n}\n\n/** @internal */\nexport const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): boolean =>\n unhandledInputs(entries).length > 0\n\n// A failure the user was shown counts as a reply, and so does a call they're asked to approve.\n/** @internal */\nexport const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n return (\n latest !== undefined &&\n !entries.some(\n ({ event, sequence }) =>\n sequence > latest.sequence &&\n (Predicate.isTagged(event, \"AssistantMessage\") ||\n Predicate.isTagged(event, \"TurnFailed\") ||\n Predicate.isTagged(event, \"ApprovalRequested\"))\n )\n )\n}\n\nconst sentToWaitOn = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(false) }))\n\n// Messages sent with `wait: false` are progress while the model kept working, like \"On it\".\nconst progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlySet<string> =>\n new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ModelOutput\")\n ? event.parts.flatMap((part) =>\n part.type === \"tool-call\" && part.name === \"send_user_message\" && Option.isSome(sentToWaitOn(part.params))\n ? [Uuid.fromKey(`message:${part.id}`)]\n : []\n )\n : []\n )\n )\n\n// The model said it was on it and stopped without the result: its last message since the user's latest input was\n// progress. An earlier message may have answered something else, such as an input that came before. A specialist still\n// working doesn't count, since its report is what comes next.\n/** @internal */\nexport const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n if (latest === undefined) return false\n\n const after = entries.flatMap(({ sequence, event }) =>\n sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []\n )\n\n const last = after.findLast((event) => Predicate.isTagged(event, \"AssistantMessage\"))\n const progress = progressMessages(entries)\n const started = after.flatMap((event) => (Predicate.isTagged(event, \"SpecialistStarted\") ? [event.runId] : []))\n\n const finished = new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"SpecialistFinished\") ? [event.runId] : []\n )\n )\n\n // A call waiting for the user's approval is what comes next, like a specialist's report.\n return (\n last !== undefined &&\n last.block === undefined &&\n progress.has(last.id) &&\n !after.some((event) => Predicate.isTagged(event, \"TurnFailed\") || Predicate.isTagged(event, \"ApprovalRequested\")) &&\n started.every((runId) => finished.has(runId))\n )\n}\n\n/** @internal */\nexport const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean =>\n entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event))\n"],"mappings":"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"}
@@ -44,16 +44,16 @@ const runBrief = Effect.fnUntraced(function* (handler, options) {
44
44
  yield* add(yield* Queue.clear(inbox));
45
45
  return yield* Ref.get(history);
46
46
  });
47
- const run$1 = {
47
+ const turn = {
48
48
  runId: options.runId,
49
49
  chatId: options.chatId,
50
50
  brief: options.brief,
51
51
  history: current,
52
- loop: (loop) => run({
53
- instructions: loop.instructions,
54
- toolkit: fromToolkitOrEmpty(loop.toolkit),
52
+ run: (run$1) => run({
53
+ instructions: run$1.instructions,
54
+ toolkit: fromToolkitOrEmpty(run$1.toolkit),
55
55
  history: current,
56
- maxSteps: loop.maxSteps ?? 8
56
+ maxSteps: run$1.maxSteps ?? 8
57
57
  }).pipe(Stream.provideService(TurnApprovals, Option.none()))
58
58
  };
59
59
  const record = Effect.fnUntraced(function* (event) {
@@ -63,7 +63,7 @@ const runBrief = Effect.fnUntraced(function* (handler, options) {
63
63
  const text = finalText(event);
64
64
  if (text.length > 0) yield* Ref.set(answer, text);
65
65
  });
66
- yield* Stream.runForEach(handler(run$1), record);
66
+ yield* Stream.runForEach(handler(turn), record);
67
67
  const text = yield* Ref.get(answer);
68
68
  yield* agentOutput(text);
69
69
  return text;
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"specialists.js","names":["Loop.run","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 = (turn: Specialist.Turn) => Stream.Stream<AssistantLoop.Event, unknown, Specialist.TurnServices>\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 turn: Specialist.Turn = {\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 run: (run) =>\n Loop.run({\n instructions: run.instructions,\n toolkit: Toolkits.fromToolkitOrEmpty(run.toolkit),\n history: current,\n maxSteps: run.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(turn), 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-assistant",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "A colleague assistant on Effect: an append-only event log per chat, an interruptible agent loop, specialists, skills, and UI blocks",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Runs the agent loop, which calls the model, runs its tool calls, and repeats.
3
3
  *
4
- * Most handlers use `turn.run` or `run.loop`. This module is for a loop of your own: {@link run}
4
+ * Most handlers use `turn.run`, in an assistant or a specialist. This module is for a loop of your own: {@link run}
5
5
  * runs the loop until the model is done, and {@link step} is one step, as a body for
6
6
  * {@link loop}.
7
7
  *
package/src/Skill.ts CHANGED
@@ -186,7 +186,7 @@ function toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {
186
186
  * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
187
187
  *
188
188
  * return turn
189
- * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
189
+ * .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
190
190
  * .pipe(Stream.provide(skills.layer))
191
191
  * })
192
192
  * )
package/src/Specialist.ts CHANGED
@@ -40,12 +40,12 @@ export interface Specialist<Name extends string = string> {
40
40
  /** The key of the specialist's handler, as `toLayer` provides it. */
41
41
  readonly Handler: Context.Key<SpecialistHandler<Name>, Built>
42
42
  /**
43
- * Provides the specialist's handler. The handler may use the {@link RunServices} and the
43
+ * Provides the specialist's handler. The handler may use the {@link TurnServices} and the
44
44
  * services its build Effect required (see {@link CheckServices}).
45
45
  */
46
46
  readonly toLayer: <R = never, RE = never, E = never, RX = never>(
47
47
  build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &
48
- CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>
48
+ CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>
49
49
  ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>
50
50
  }
51
51
 
@@ -108,21 +108,21 @@ export interface SpecialistHandler<Name extends string> {
108
108
  export type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never
109
109
 
110
110
  /**
111
- * The services a run provides to its handler, which are its chat, the chat's runner, and the
111
+ * The services a turn provides to its handler, which are its chat, the chat's runner, and the
112
112
  * model.
113
113
  *
114
114
  * @category utility types
115
115
  * @since 0.1.0
116
116
  */
117
- export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel
117
+ export type TurnServices = CurrentChat | Runner | LanguageModel.LanguageModel
118
118
 
119
119
  /**
120
- * Represents the options for `run.loop`.
120
+ * Represents the options for `turn.run`.
121
121
  *
122
122
  * @category models
123
123
  * @since 0.1.0
124
124
  */
125
- export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
125
+ export interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
126
126
  readonly instructions: string | Instructions.Resolved
127
127
  /**
128
128
  * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
@@ -134,12 +134,13 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
134
134
  }
135
135
 
136
136
  /**
137
- * Represents one run of a specialist on a brief, as its handler sees it.
137
+ * Represents one turn of a specialist's run, as its handler sees it. The first turn works on the
138
+ * brief, and each later one on the messages sent to the run since (follow-ups and steering).
138
139
  *
139
140
  * @category models
140
141
  * @since 0.1.0
141
142
  */
142
- export interface Run {
143
+ export interface Turn {
143
144
  readonly runId: RunId
144
145
  /** The chat the run belongs to, which is the chat of the turn that spawned it. */
145
146
  readonly chatId: ChatId
@@ -150,8 +151,8 @@ export interface Run {
150
151
  */
151
152
  readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>
152
153
  /** Runs the agent loop over the run's history, with the given instructions and tools. */
153
- readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(
154
- options: LoopOptions<T>
154
+ readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(
155
+ options: RunOptions<T>
155
156
  ) => Stream.Stream<
156
157
  AssistantLoop.Event,
157
158
  AiError.AiError,
@@ -170,7 +171,7 @@ export interface Run {
170
171
  * @since 0.1.0
171
172
  */
172
173
  export type Handler<R = never, RE = R> = (
173
- run: Run
174
+ turn: Turn
174
175
  ) =>
175
176
  | Stream.Stream<AssistantLoop.Event, unknown, R>
176
177
  | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>
@@ -186,7 +187,7 @@ export type Handler<R = never, RE = R> = (
186
187
  */
187
188
  export interface Built {
188
189
  readonly open: Effect.Effect<
189
- (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,
190
+ (turn: Turn) => Stream.Stream<AssistantLoop.Event, unknown, TurnServices>,
190
191
  never,
191
192
  Scope.Scope | CurrentChat
192
193
  >
@@ -203,7 +204,7 @@ const toStream = <R, RE>(
203
204
  *
204
205
  * **Details**
205
206
  *
206
- * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the
207
+ * The specialist's `toLayer` provides its handler, which is usually `turn.run` with the
207
208
  * specialist's own instructions and tools. List the specialist in `Assistant.make`'s
208
209
  * `specialists`, and provide its layer to the assistant's.
209
210
  *
@@ -217,8 +218,8 @@ const toStream = <R, RE>(
217
218
  * description: "Researches past campaigns and orders. Give it the question and any context."
218
219
  * })
219
220
  *
220
- * const ResearchLive = Research.toLayer((run) =>
221
- * run.loop({
221
+ * const ResearchLive = Research.toLayer((turn) =>
222
+ * turn.run({
222
223
  * instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
223
224
  * toolkit: ResearchTools
224
225
  * })
@@ -248,7 +249,7 @@ export const make = <const Name extends string>(
248
249
  // services.
249
250
  function toLayer<R = never, RE = never, E = never, RX = never>(
250
251
  build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &
251
- CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>
252
+ CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>
252
253
  ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>
253
254
  function toLayer(
254
255
  build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>
@@ -264,7 +265,7 @@ export const make = <const Name extends string>(
264
265
  open: Effect.gen(function* () {
265
266
  const handler = Effect.isEffect(build) ? yield* build : build
266
267
 
267
- return (run: Run) => Stream.provideContext(toStream(handler(run)), context)
268
+ return (turn: Turn) => Stream.provideContext(toStream(handler(turn)), context)
268
269
  }).pipe(Effect.provideContext(context), Effect.orDie)
269
270
  }
270
271
  })
@@ -321,8 +321,9 @@ const progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Read
321
321
  )
322
322
  )
323
323
 
324
- // The model said it was on it and stopped without the result: only progress messages since the user's latest input.
325
- // A specialist still working doesn't count, since its report is what comes next.
324
+ // The model said it was on it and stopped without the result: its last message since the user's latest input was
325
+ // progress. An earlier message may have answered something else, such as an input that came before. A specialist still
326
+ // working doesn't count, since its report is what comes next.
326
327
  /** @internal */
327
328
  export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {
328
329
  const latest = entries.findLast(
@@ -336,7 +337,7 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
336
337
  sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []
337
338
  )
338
339
 
339
- const messages = after.flatMap((event) => (Predicate.isTagged(event, "AssistantMessage") ? [event] : []))
340
+ const last = after.findLast((event) => Predicate.isTagged(event, "AssistantMessage"))
340
341
  const progress = progressMessages(entries)
341
342
  const started = after.flatMap((event) => (Predicate.isTagged(event, "SpecialistStarted") ? [event.runId] : []))
342
343
 
@@ -348,8 +349,9 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
348
349
 
349
350
  // A call waiting for the user's approval is what comes next, like a specialist's report.
350
351
  return (
351
- messages.length > 0 &&
352
- messages.every((message) => message.block === undefined && progress.has(message.id)) &&
352
+ last !== undefined &&
353
+ last.block === undefined &&
354
+ progress.has(last.id) &&
353
355
  !after.some((event) => Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")) &&
354
356
  started.every((runId) => finished.has(runId))
355
357
  )
@@ -30,7 +30,7 @@ import * as Toolkits from "./toolkit.ts"
30
30
  import type { Writer } from "./writer.ts"
31
31
 
32
32
  /** @internal */
33
- export type RunHandler = (run: Specialist.Run) => Stream.Stream<AssistantLoop.Event, unknown, Specialist.RunServices>
33
+ export type RunHandler = (turn: Specialist.Turn) => Stream.Stream<AssistantLoop.Event, unknown, Specialist.TurnServices>
34
34
 
35
35
  /** @internal */
36
36
  export const briefText = (brief: Specialist.Brief): string =>
@@ -75,18 +75,18 @@ export const runBrief = Effect.fnUntraced(
75
75
  return yield* Ref.get(history)
76
76
  })
77
77
 
78
- const run: Specialist.Run = {
78
+ const turn: Specialist.Turn = {
79
79
  runId: options.runId,
80
80
  chatId: options.chatId,
81
81
  brief: options.brief,
82
82
  history: current,
83
83
  // A run can't wait for the user's approval: a call that needs it fails, and the run reads why.
84
- loop: (loop) =>
84
+ run: (run) =>
85
85
  Loop.run({
86
- instructions: loop.instructions,
87
- toolkit: Toolkits.fromToolkitOrEmpty(loop.toolkit),
86
+ instructions: run.instructions,
87
+ toolkit: Toolkits.fromToolkitOrEmpty(run.toolkit),
88
88
  history: current,
89
- maxSteps: loop.maxSteps ?? 8
89
+ maxSteps: run.maxSteps ?? 8
90
90
  }).pipe(Stream.provideService(Loop.TurnApprovals, Option.none()))
91
91
  }
92
92
 
@@ -102,7 +102,7 @@ export const runBrief = Effect.fnUntraced(
102
102
  if (text.length > 0) yield* Ref.set(answer, text)
103
103
  })
104
104
 
105
- yield* Stream.runForEach(handler(run), record)
105
+ yield* Stream.runForEach(handler(turn), record)
106
106
 
107
107
  const text = yield* Ref.get(answer)
108
108