effect-assistant 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Assistant.d.ts +5 -11
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantEvent.d.ts +66 -7
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +74 -9
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +4 -2
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +4 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.d.ts +63 -1
- package/dist/Block.d.ts.map +1 -1
- package/dist/Block.js +63 -2
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +25 -3
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +14 -12
- package/dist/BlockKit.js.map +1 -1
- package/dist/Compaction.d.ts +1 -1
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +11 -4
- package/dist/Compaction.js.map +1 -1
- package/dist/Skill.d.ts +85 -43
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +93 -44
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +6 -9
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +44 -6
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +87 -16
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +146 -0
- package/dist/ToolApproval.d.ts.map +1 -0
- package/dist/ToolApproval.js +201 -0
- package/dist/ToolApproval.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +6 -4
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolResult.d.ts +56 -0
- package/dist/ToolResult.d.ts.map +1 -0
- package/dist/ToolResult.js +49 -0
- package/dist/ToolResult.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -4
- package/dist/internal/chat.js +4 -1
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/history.js +28 -3
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +10 -5
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +60 -21
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +9 -16
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +11 -7
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +74 -21
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +39 -6
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -16
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +7 -13
- package/src/AssistantEvent.ts +73 -8
- package/src/AssistantLoop.ts +4 -2
- package/src/Block.ts +82 -1
- package/src/BlockKit.ts +37 -4
- package/src/Compaction.ts +19 -2
- package/src/Skill.ts +134 -57
- package/src/Specialist.ts +8 -11
- package/src/Timeline.ts +120 -21
- package/src/ToolApproval.ts +249 -0
- package/src/ToolDiscovery.ts +8 -3
- package/src/ToolResult.ts +80 -0
- package/src/index.ts +10 -0
- package/src/internal/chat.ts +11 -1
- package/src/internal/history.ts +58 -3
- package/src/internal/loop.ts +138 -35
- package/src/internal/specialists.ts +7 -13
- package/src/internal/toolkit.ts +102 -35
- package/src/internal/tools.ts +81 -5
- package/src/internal/turns.ts +31 -10
- package/dist/internal/skills.js +0 -33
- package/dist/internal/skills.js.map +0 -1
- package/src/internal/skills.ts +0 -48
package/dist/internal/turns.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { ToolResult, TurnCompleted, TurnFailed, TurnStarted, isLibraryEvent } from "../AssistantEvent.js";
|
|
2
|
+
import { fromEntries } from "../ToolApproval.js";
|
|
2
3
|
import { decodeEntries } from "./events.js";
|
|
3
4
|
import { toJsonText } from "./schema.js";
|
|
4
|
-
import { fromEntries, mayHaveRunOutput, notRunOutput, owesDelivery, owesReply, unansweredCalls, unhandledInputs, unreadAfter, userText } from "./history.js";
|
|
5
|
+
import { fromEntries as fromEntries$1, mayHaveRunOutput, notRunOutput, owesDelivery, owesReply, unansweredCalls, unhandledInputs, unreadAfter, userText } from "./history.js";
|
|
5
6
|
import { agentInput, agentOutput, agentSpan, currentTraceId } from "./telemetry.js";
|
|
6
|
-
import { duplicateNames, fromToolkitOrEmpty, merge, provide, runOnly } from "./toolkit.js";
|
|
7
|
+
import { duplicateNames, fromToolkitOrEmpty, merge, provide, runOnly, showing } from "./toolkit.js";
|
|
7
8
|
import { makeMessaging, sendUserMessageName, waitToolName } from "./tools.js";
|
|
8
|
-
import { TurnTools, run as run$1 } from "./loop.js";
|
|
9
|
-
import {
|
|
9
|
+
import { TurnApprovals, TurnTools, run as run$1 } from "./loop.js";
|
|
10
|
+
import { toolkit } from "./specialists.js";
|
|
11
|
+
import { toolNames } from "../BlockKit.js";
|
|
10
12
|
import { protocol } from "../Instructions.js";
|
|
11
|
-
import { available, toTools } from "./skills.js";
|
|
12
13
|
import { mapText } from "./instructions.js";
|
|
13
|
-
import { toolkit } from "./specialists.js";
|
|
14
14
|
import "effect/Context";
|
|
15
15
|
import * as Effect from "effect/Effect";
|
|
16
16
|
import * as Predicate from "effect/Predicate";
|
|
@@ -18,6 +18,7 @@ import * as Schema from "effect/Schema";
|
|
|
18
18
|
import * as Stream from "effect/Stream";
|
|
19
19
|
import * as Exit from "effect/Exit";
|
|
20
20
|
import * as Option from "effect/Option";
|
|
21
|
+
import * as Response from "effect/ai/Response";
|
|
21
22
|
import * as Ref from "effect/Ref";
|
|
22
23
|
import * as Cause from "effect/Cause";
|
|
23
24
|
import { identity } from "effect/Function";
|
|
@@ -56,11 +57,17 @@ const failureMessage = (cause) => Option.match(Schema.decodeUnknownOption(Provid
|
|
|
56
57
|
onNone: () => Cause.pretty(cause),
|
|
57
58
|
onSome: ({ _tag, raw }) => `${_tag ?? "Error"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`
|
|
58
59
|
});
|
|
60
|
+
const approvedCalls = (entries) => [...fromEntries(entries).values()].flatMap(({ request, status, outcome }) => status === "approved" && outcome === void 0 ? [Response.makePart("tool-call", {
|
|
61
|
+
id: request.callId,
|
|
62
|
+
name: request.tool,
|
|
63
|
+
params: request.params,
|
|
64
|
+
providerExecuted: false
|
|
65
|
+
})] : []);
|
|
59
66
|
const lastSeq = (entries, otherwise) => entries.at(-1)?.seq ?? otherwise;
|
|
60
67
|
const closeOpenCalls = Effect.fnUntraced(function* (chat, startSeq) {
|
|
61
68
|
const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq);
|
|
62
69
|
const started = new Set(entries.flatMap(({ event }) => Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []));
|
|
63
|
-
const open = unansweredCalls(fromEntries(entries));
|
|
70
|
+
const open = unansweredCalls(fromEntries$1(entries));
|
|
64
71
|
if (open.length === 0) return;
|
|
65
72
|
yield* chat.append(open.map((call) => ToolResult.make({
|
|
66
73
|
callId: call.id,
|
|
@@ -95,12 +102,13 @@ const failTurn = Effect.fnUntraced(function* (chat, cause) {
|
|
|
95
102
|
const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
96
103
|
const { definition, builtins } = registration;
|
|
97
104
|
const { app } = definition;
|
|
105
|
+
const shownTools = toolNames(definition.blocks);
|
|
98
106
|
const running = (stream) => Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)));
|
|
99
107
|
const history = Effect.fnUntraced(function* (view = identity) {
|
|
100
108
|
yield* checkOpen;
|
|
101
109
|
const all = yield* chat.entries;
|
|
102
110
|
return {
|
|
103
|
-
messages: fromEntries(view(decodeEntries(app, all))),
|
|
111
|
+
messages: fromEntries$1(view(decodeEntries(app, all))),
|
|
104
112
|
throughSeq: lastSeq(all, startSeq)
|
|
105
113
|
};
|
|
106
114
|
});
|
|
@@ -120,12 +128,13 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
|
120
128
|
yield* checkOpen;
|
|
121
129
|
const entries = yield* chat.entries;
|
|
122
130
|
return {
|
|
123
|
-
messages: [...fromEntries(options.view(decodeEntries(app, entries))), userText(nudge)],
|
|
131
|
+
messages: [...fromEntries$1(options.view(decodeEntries(app, entries))), userText(nudge)],
|
|
124
132
|
throughSeq: lastSeq(entries, startSeq)
|
|
125
133
|
};
|
|
126
134
|
}),
|
|
127
|
-
maxSteps: 2
|
|
128
|
-
|
|
135
|
+
maxSteps: 2,
|
|
136
|
+
toolChoice: "required"
|
|
137
|
+
}).pipe(Stream.provideService(TurnApprovals, Option.none()));
|
|
129
138
|
}));
|
|
130
139
|
const replyIfOwed = (options) => running(replyPass({
|
|
131
140
|
instructions: instructions(options.instructions),
|
|
@@ -134,11 +143,10 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
|
134
143
|
nudge: options.nudge
|
|
135
144
|
}));
|
|
136
145
|
const prepare = (options) => {
|
|
137
|
-
const own = fromToolkitOrEmpty(options.toolkit);
|
|
138
|
-
const skills = available(options.skills ?? [], [...builtins.definitions, ...own.definitions]);
|
|
146
|
+
const own = showing(fromToolkitOrEmpty(options.toolkit), shownTools);
|
|
139
147
|
return {
|
|
140
|
-
system:
|
|
141
|
-
toolkit: merge(
|
|
148
|
+
system: instructions(options.instructions),
|
|
149
|
+
toolkit: merge(builtins, own),
|
|
142
150
|
view: options.history ?? identity
|
|
143
151
|
};
|
|
144
152
|
};
|
|
@@ -183,7 +191,7 @@ const run = Effect.fnUntraced(function* (options) {
|
|
|
183
191
|
const ranLoop = yield* Ref.make(false);
|
|
184
192
|
const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop);
|
|
185
193
|
const record = (event) => chat.append([event]);
|
|
186
|
-
yield* options.handler(turn).pipe(Stream.runForEach(record), Effect.onExit((exit) => Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq)), Effect.provideService(TurnTools, Option.some(provide(options.registration.builtins, options.services))), Effect.provideContext(options.services), Effect.matchCauseEffect({
|
|
194
|
+
yield* options.handler(turn).pipe(Stream.runForEach(record), Effect.onExit((exit) => Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq)), Effect.provideService(TurnTools, Option.some(provide(options.registration.builtins, options.services))), Effect.provideService(TurnApprovals, Option.some({ approved: Effect.map(chat.entries, approvedCalls) })), Effect.provideContext(options.services), Effect.matchCauseEffect({
|
|
187
195
|
onSuccess: Effect.fnUntraced(function* () {
|
|
188
196
|
const all = yield* chat.entries;
|
|
189
197
|
yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turns.js","names":["Toolkits.merge","Tools.makeMessaging","Specialists.toolkit","History.unansweredCalls","History.fromEntries","History.mayHaveRunOutput","History.notRunOutput","AssistantEvent.isLibraryEvent","History.owesReply","History.unreadAfter","AppEvents.decodeEntries","Instructions.protocol","History.owesDelivery","Loop.run","Toolkits.runOnly","Tools.sendUserMessageName","Tools.waitToolName","History.userText","Toolkits.fromToolkitOrEmpty","Skills.available","Skill.withCatalog","Skills.toTools","Toolkits.duplicateNames","Telemetry.agentInput","History.unhandledInputs","Telemetry.currentTraceId","Loop.TurnTools","Toolkits.provide","Telemetry.agentOutput","Telemetry.agentSpan"],"sources":["../../src/internal/turns.ts"],"sourcesContent":["import type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport { identity } from \"effect/Function\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type * as AssistantLoop from \"../AssistantLoop.ts\"\nimport * as Loop from \"./loop.ts\"\nimport type * as BlockKit from \"../BlockKit.ts\"\nimport type { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Instructions from \"../Instructions.ts\"\nimport * as Skill from \"../Skill.ts\"\nimport * as Skills from \"./skills.ts\"\nimport type * as Specialist from \"../Specialist.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport { mapText } from \"./instructions.ts\"\nimport { toJsonText } from \"./schema.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Telemetry from \"./telemetry.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport type { Writer } from \"./writer.ts\"\nimport * as Tools from \"./tools.ts\"\n\n/** @internal */\nexport const silentTurnMessage = \"I finished without sending a reply.\"\n\n/** @internal */\nexport const defaultReplyNudge =\n \"You haven't replied to the user's latest message yet. Reply now with send_user_message.\"\n\n/** @internal */\nexport const deliveryNudge =\n \"You told the user you're on it, but stopped without giving them the result. Send it now with send_user_message; if part of it is still running, say what and that you'll report back. If they already have everything, call wait.\"\n\n/** @internal */\nexport interface Definition {\n readonly name: string\n readonly blocks: BlockKit.BlockKit | undefined\n readonly specialists: ReadonlyArray<Specialist.Any>\n readonly app: AppEvents.AppEvents\n}\n\n// Once its build ran the layer's services are provided, so its streams need only what a turn provides.\n/** @internal */\nexport type TurnHandler = (\n turn: Assistant.Turn<any>\n) => Stream.Stream<AssistantLoop.Event, unknown, Assistant.TurnServices | LanguageModel.LanguageModel>\n\n// The chat `CurrentChat` gives, with its entries typed as the assistant's decode returns them: the assistant's typed\n// chat is this one, so it needs no check of its own.\n/** @internal */\nexport interface TypedChat {\n readonly chatId: ChatId\n readonly entries: Effect.Effect<AppEvents.DecodedEntries>\n readonly append: CurrentChat[\"Service\"][\"append\"]\n}\n\n/** @internal */\nexport interface Registration {\n readonly definition: Definition\n readonly model: Context.Context<LanguageModel.LanguageModel>\n readonly open: (chat: TypedChat) => Effect.Effect<TurnHandler, never, Scope.Scope | CurrentChat>\n readonly specialists: ReadonlyArray<{\n readonly specialist: Specialist.Any\n readonly built: Specialist.Built\n }>\n readonly builtins: Toolkits.Tools<Assistant.TurnServices>\n}\n\n/** @internal */\nexport const register = Effect.fnUntraced(function* (\n definition: Definition,\n options: { readonly open: Registration[\"open\"] }\n) {\n const model = yield* Effect.context<LanguageModel.LanguageModel>()\n\n const specialists = yield* Effect.forEach(\n definition.specialists,\n Effect.fnUntraced(function* (specialist) {\n const built = yield* Effect.serviceOption(specialist.Handler)\n\n if (Option.isNone(built)) {\n return yield* Effect.die(\n `No handler for the ${specialist.name} specialist: provide ${specialist.name}.toLayer.`\n )\n }\n\n return { specialist, built: built.value }\n })\n )\n\n const builtins = Toolkits.merge(\n yield* Tools.makeMessaging(definition.blocks),\n yield* Specialists.toolkit(definition.specialists)\n )\n\n const registration: Registration = {\n definition,\n model,\n open: options.open,\n specialists,\n builtins\n }\n\n return registration\n})\n\nconst ProviderError = Schema.Struct({ _tag: Schema.optional(Schema.String), raw: Schema.Unknown })\n\n// Provider errors keep their response body.\nconst failureMessage = (cause: Cause.Cause<unknown>): string =>\n Option.match(Schema.decodeUnknownOption(ProviderError)(Cause.squash(cause)), {\n onNone: () => Cause.pretty(cause),\n onSome: ({ _tag, raw }) => `${_tag ?? \"Error\"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`\n })\n\nconst lastSeq = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) => entries.at(-1)?.seq ?? otherwise\n\n// The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a\n// shutdown) leaves its calls open instead: the next turn runs them again.\nconst closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: number) {\n const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq)\n\n const started = new Set(\n entries.flatMap(({ event }) => (Predicate.isTagged(event, \"ToolStarted\") ? [event.callId] : []))\n )\n\n const open = History.unansweredCalls(History.fromEntries(entries))\n\n if (open.length === 0) return\n\n yield* chat.append(\n open.map((call) =>\n AssistantEvent.ToolResult.make({\n callId: call.id,\n tool: call.name,\n outcome: \"failure\",\n ...(started.has(call.id)\n ? { result: History.mayHaveRunOutput, reason: \"MayHaveRun\" }\n : { result: History.notRunOutput, reason: \"NotRun\" })\n })\n )\n )\n})\n\n// The inputs before the turn's `TurnStarted` (at `startSeq`) woke it; past that, what its model outputs record\n// reading.\nconst handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSeq: number) =>\n entries.reduce(\n (through, { seq, event }) =>\n seq > startSeq &&\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ModelOutput\") &&\n event.seenThroughSeq !== undefined\n ? Math.max(through, event.seenThroughSeq)\n : through,\n startSeq\n )\n\n// A turn that ran the loop fails visibly when the user's latest input got no reply; an input that arrived after what\n// its model read gets the next turn, which replies. A handler that ended without the loop decided that itself.\nconst finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranLoop: boolean) {\n const entries = yield* chat.entries\n\n const end =\n ranLoop && History.owesReply(entries, read) && !History.unreadAfter(entries, read)\n ? AssistantEvent.TurnFailed.make({ throughSeq: read, message: silentTurnMessage })\n : AssistantEvent.TurnCompleted.make({ throughSeq: read })\n\n yield* chat.append([end])\n})\n\n// Everything stored so far counts as read, so a failing input isn't retried forever.\nconst failTurn = Effect.fnUntraced(function* (chat: Writer, cause: Cause.Cause<unknown>) {\n yield* Effect.logError(\"assistant turn failed\", cause)\n\n const last = lastSeq(yield* chat.entries, 0)\n\n yield* chat.append([AssistantEvent.TurnFailed.make({ throughSeq: last, message: failureMessage(cause) })])\n})\n\nconst makeTurnApi = (\n registration: Registration,\n chat: Writer,\n startSeq: number,\n checkOpen: Effect.Effect<void>,\n ranLoop: Ref.Ref<boolean>\n): Assistant.Turn<any> => {\n const { definition, builtins } = registration\n const { app } = definition\n\n const running = <A, E, R>(stream: Stream.Stream<A, E, R>) =>\n Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)))\n\n // With how far into the chat it goes: the step records it on the model's output.\n // Read before every model call: a chat the app deleted meanwhile stops here.\n const history = Effect.fnUntraced(function* (view: Assistant.HistoryView<any> = identity) {\n yield* checkOpen\n\n const all = yield* chat.entries\n\n const read: AssistantLoop.ChatHistory = {\n messages: History.fromEntries(view(AppEvents.decodeEntries(app, all))),\n throughSeq: lastSeq(all, startSeq)\n }\n\n return read\n })\n\n const instructions = (given: string | Instructions.Resolved) =>\n mapText(given, (text) => `${Instructions.protocol}\\n\\n${text}`)\n\n // The loop's own prompt and tools, so the prompt stays cached, with the nudge at the end.\n const replyPass = <R>(options: {\n readonly instructions: Instructions.Resolved\n readonly toolkit: Toolkits.Tools<R>\n readonly view: Assistant.HistoryView<any>\n readonly nudge: string | undefined\n }) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const all = yield* chat.entries\n const through = handledThrough(all, startSeq)\n\n // Something arrived after what the loop's model read: the next turn reads it with the full loop, and replies.\n if (History.unreadAfter(all, through)) return Stream.empty\n\n const owed = History.owesReply(all, through)\n const undelivered = !owed && History.owesDelivery(all, through)\n\n if (!owed && !undelivered) return Stream.empty\n\n const nudge = undelivered ? deliveryNudge : (options.nudge ?? defaultReplyNudge)\n\n return Loop.run({\n instructions: options.instructions,\n // A reply that's owed can only be sent; a result it may have given may also be for nobody.\n toolkit: Toolkits.runOnly(\n options.toolkit,\n owed ? [Tools.sendUserMessageName] : [Tools.sendUserMessageName, Tools.waitToolName],\n \"Not now: reply to the user with send_user_message.\"\n ),\n history: Effect.gen(function* () {\n yield* checkOpen\n\n const entries = yield* chat.entries\n\n return {\n messages: [\n ...History.fromEntries(options.view(AppEvents.decodeEntries(app, entries))),\n History.userText(nudge)\n ],\n throughSeq: lastSeq(entries, startSeq)\n }\n }),\n // A second step, for a reply after a call it was refused.\n maxSteps: 2\n })\n })\n )\n\n const replyIfOwed = (options: Assistant.ReplyOptions) =>\n running(\n replyPass({\n instructions: instructions(options.instructions),\n toolkit: builtins,\n view: identity,\n nudge: options.nudge\n })\n )\n\n const prepare = <T extends Record<string, Tool.Any>, RS>(options: Assistant.LoopOptions<T, RS, any>) => {\n const own = Toolkits.fromToolkitOrEmpty(options.toolkit)\n const skills = Skills.available(options.skills ?? [], [...builtins.definitions, ...own.definitions])\n\n return {\n system: mapText(instructions(options.instructions), (text) => Skill.withCatalog(text, skills)),\n toolkit: Toolkits.merge(Toolkits.merge(builtins, Skills.toTools(skills)), own),\n view: options.history ?? identity\n }\n }\n\n return {\n chatId: chat.chatId,\n history,\n instructions,\n replyIfOwed,\n loop: (options) => {\n const { system, toolkit, view } = prepare(options)\n const twice = Toolkits.duplicateNames(toolkit)\n\n const loop =\n twice.length > 0\n ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(\", \")}`)\n : Loop.run({\n instructions: system,\n toolkit,\n history: history(view),\n maxSteps: options.maxSteps ?? 16\n })\n\n return running(Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge })))\n }\n }\n}\n\n// Every event the handler's loop emits is recorded before the loop is pulled again.\n/** @internal */\nexport const run = Effect.fnUntraced(\n function* (options: {\n readonly registration: Registration\n readonly handler: TurnHandler\n readonly chat: Writer\n readonly services: Context.Context<Assistant.TurnServices | LanguageModel.LanguageModel>\n // A chat the app deleted is deleted here, and the turn stops.\n readonly checkOpen: Effect.Effect<void>\n }) {\n const { chat } = options\n const before = yield* chat.entries\n\n yield* Telemetry.agentInput(History.unhandledInputs(before))\n\n const assistant = options.registration.definition.name\n const traceId = yield* Telemetry.currentTraceId\n\n const started = yield* chat.append([\n AssistantEvent.TurnStarted.make(\n Option.match(traceId, {\n onNone: () => ({ assistant }),\n onSome: (id) => ({ assistant, traceId: id })\n })\n )\n ])\n\n const startSeq = lastSeq(started, 0)\n const ranLoop = yield* Ref.make(false)\n const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop)\n\n const record = (event: AssistantLoop.Event) => chat.append([event])\n\n yield* options.handler(turn).pipe(\n Stream.runForEach(record),\n Effect.onExit((exit) => (Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq))),\n // A loop the app writes gets the assistant's own tools too, with this turn's services.\n Effect.provideService(\n Loop.TurnTools,\n Option.some(Toolkits.provide(options.registration.builtins, options.services))\n ),\n Effect.provideContext(options.services),\n Effect.matchCauseEffect({\n onSuccess: Effect.fnUntraced(function* () {\n const all = yield* chat.entries\n\n yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop))\n }),\n onFailure: (cause) => failTurn(chat, cause)\n })\n )\n\n // The trace's output is what the user saw this turn, and a failure if any.\n const shown = (yield* chat.entries).flatMap(({ event, seq }) =>\n seq > startSeq && (Predicate.isTagged(event, \"AssistantMessage\") || Predicate.isTagged(event, \"TurnFailed\"))\n ? [event]\n : []\n )\n\n yield* Telemetry.agentOutput(shown)\n },\n (effect, options) =>\n effect.pipe(\n Telemetry.agentSpan(options.registration.definition.name, {\n chatId: options.chat.chatId,\n root: true\n })\n )\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"turns.js","names":["Toolkits.merge","Tools.makeMessaging","Specialists.toolkit","ToolApproval.fromEntries","History.unansweredCalls","History.fromEntries","History.mayHaveRunOutput","History.notRunOutput","AssistantEvent.isLibraryEvent","History.owesReply","History.unreadAfter","BlockKit.toolNames","AppEvents.decodeEntries","Instructions.protocol","History.owesDelivery","Loop.run","Toolkits.runOnly","Tools.sendUserMessageName","Tools.waitToolName","History.userText","Loop.TurnApprovals","Toolkits.showing","Toolkits.fromToolkitOrEmpty","Toolkits.duplicateNames","Telemetry.agentInput","History.unhandledInputs","Telemetry.currentTraceId","Loop.TurnTools","Toolkits.provide","Telemetry.agentOutput","Telemetry.agentSpan"],"sources":["../../src/internal/turns.ts"],"sourcesContent":["import type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport * as Response from \"effect/ai/Response\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport { identity } from \"effect/Function\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type * as AssistantLoop from \"../AssistantLoop.ts\"\nimport * as Loop from \"./loop.ts\"\nimport * as BlockKit from \"../BlockKit.ts\"\nimport type { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Instructions from \"../Instructions.ts\"\nimport * as ToolApproval from \"../ToolApproval.ts\"\nimport type * as Specialist from \"../Specialist.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport { mapText } from \"./instructions.ts\"\nimport { toJsonText } from \"./schema.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Telemetry from \"./telemetry.ts\"\nimport * as Toolkits from \"./toolkit.ts\"\nimport type { Writer } from \"./writer.ts\"\nimport * as Tools from \"./tools.ts\"\n\n/** @internal */\nexport const silentTurnMessage = \"I finished without sending a reply.\"\n\n/** @internal */\nexport const defaultReplyNudge =\n \"You haven't replied to the user's latest message yet. Reply now with send_user_message.\"\n\n/** @internal */\nexport const deliveryNudge =\n \"You told the user you're on it, but stopped without giving them the result. Send it now with send_user_message; if part of it is still running, say what and that you'll report back. If they already have everything, call wait.\"\n\n/** @internal */\nexport interface Definition {\n readonly name: string\n readonly blocks: BlockKit.BlockKit | undefined\n readonly specialists: ReadonlyArray<Specialist.Any>\n readonly app: AppEvents.AppEvents\n}\n\n// Once its build ran the layer's services are provided, so its streams need only what a turn provides.\n/** @internal */\nexport type TurnHandler = (\n turn: Assistant.Turn<any>\n) => Stream.Stream<AssistantLoop.Event, unknown, Assistant.TurnServices | LanguageModel.LanguageModel>\n\n// The chat `CurrentChat` gives, with its entries typed as the assistant's decode returns them: the assistant's typed\n// chat is this one, so it needs no check of its own.\n/** @internal */\nexport interface TypedChat {\n readonly chatId: ChatId\n readonly entries: Effect.Effect<AppEvents.DecodedEntries>\n readonly append: CurrentChat[\"Service\"][\"append\"]\n}\n\n/** @internal */\nexport interface Registration {\n readonly definition: Definition\n readonly model: Context.Context<LanguageModel.LanguageModel>\n readonly open: (chat: TypedChat) => Effect.Effect<TurnHandler, never, Scope.Scope | CurrentChat>\n readonly specialists: ReadonlyArray<{\n readonly specialist: Specialist.Any\n readonly built: Specialist.Built\n }>\n readonly builtins: Toolkits.Tools<Assistant.TurnServices>\n}\n\n/** @internal */\nexport const register = Effect.fnUntraced(function* (\n definition: Definition,\n options: { readonly open: Registration[\"open\"] }\n) {\n const model = yield* Effect.context<LanguageModel.LanguageModel>()\n\n const specialists = yield* Effect.forEach(\n definition.specialists,\n Effect.fnUntraced(function* (specialist) {\n const built = yield* Effect.serviceOption(specialist.Handler)\n\n if (Option.isNone(built)) {\n return yield* Effect.die(\n `No handler for the ${specialist.name} specialist: provide ${specialist.name}.toLayer.`\n )\n }\n\n return { specialist, built: built.value }\n })\n )\n\n const builtins = Toolkits.merge(\n yield* Tools.makeMessaging(definition.blocks),\n yield* Specialists.toolkit(definition.specialists)\n )\n\n const registration: Registration = {\n definition,\n model,\n open: options.open,\n specialists,\n builtins\n }\n\n return registration\n})\n\nconst ProviderError = Schema.Struct({ _tag: Schema.optional(Schema.String), raw: Schema.Unknown })\n\n// Provider errors keep their response body.\nconst failureMessage = (cause: Cause.Cause<unknown>): string =>\n Option.match(Schema.decodeUnknownOption(ProviderError)(Cause.squash(cause)), {\n onNone: () => Cause.pretty(cause),\n onSome: ({ _tag, raw }) => `${_tag ?? \"Error\"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`\n })\n\n// Approved, and not run yet: also one a crash cut off while it ran, which runs again with the same id.\nconst approvedCalls = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlyArray<AssistantEvent.ToolCallPart> =>\n [...ToolApproval.fromEntries(entries).values()].flatMap(({ request, status, outcome }) =>\n status === \"approved\" && outcome === undefined\n ? [\n Response.makePart(\"tool-call\", {\n id: request.callId,\n name: request.tool,\n params: request.params,\n providerExecuted: false\n })\n ]\n : []\n )\n\nconst lastSeq = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) => entries.at(-1)?.seq ?? otherwise\n\n// The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a\n// shutdown) leaves its calls open instead: the next turn runs them again.\nconst closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: number) {\n const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq)\n\n const started = new Set(\n entries.flatMap(({ event }) => (Predicate.isTagged(event, \"ToolStarted\") ? [event.callId] : []))\n )\n\n const open = History.unansweredCalls(History.fromEntries(entries))\n\n if (open.length === 0) return\n\n yield* chat.append(\n open.map((call) =>\n AssistantEvent.ToolResult.make({\n callId: call.id,\n tool: call.name,\n outcome: \"failure\",\n ...(started.has(call.id)\n ? { result: History.mayHaveRunOutput, reason: \"MayHaveRun\" }\n : { result: History.notRunOutput, reason: \"NotRun\" })\n })\n )\n )\n})\n\n// The inputs before the turn's `TurnStarted` (at `startSeq`) woke it; past that, what its model outputs record\n// reading.\nconst handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSeq: number) =>\n entries.reduce(\n (through, { seq, event }) =>\n seq > startSeq &&\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ModelOutput\") &&\n event.seenThroughSeq !== undefined\n ? Math.max(through, event.seenThroughSeq)\n : through,\n startSeq\n )\n\n// A turn that ran the loop fails visibly when the user's latest input got no reply; an input that arrived after what\n// its model read gets the next turn, which replies. A handler that ended without the loop decided that itself.\nconst finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranLoop: boolean) {\n const entries = yield* chat.entries\n\n const end =\n ranLoop && History.owesReply(entries, read) && !History.unreadAfter(entries, read)\n ? AssistantEvent.TurnFailed.make({ throughSeq: read, message: silentTurnMessage })\n : AssistantEvent.TurnCompleted.make({ throughSeq: read })\n\n yield* chat.append([end])\n})\n\n// Everything stored so far counts as read, so a failing input isn't retried forever.\nconst failTurn = Effect.fnUntraced(function* (chat: Writer, cause: Cause.Cause<unknown>) {\n yield* Effect.logError(\"assistant turn failed\", cause)\n\n const last = lastSeq(yield* chat.entries, 0)\n\n yield* chat.append([AssistantEvent.TurnFailed.make({ throughSeq: last, message: failureMessage(cause) })])\n})\n\nconst makeTurnApi = (\n registration: Registration,\n chat: Writer,\n startSeq: number,\n checkOpen: Effect.Effect<void>,\n ranLoop: Ref.Ref<boolean>\n): Assistant.Turn<any> => {\n const { definition, builtins } = registration\n const { app } = definition\n const shownTools = BlockKit.toolNames(definition.blocks)\n\n const running = <A, E, R>(stream: Stream.Stream<A, E, R>) =>\n Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)))\n\n // With how far into the chat it goes: the step records it on the model's output.\n // Read before every model call: a chat the app deleted meanwhile stops here.\n const history = Effect.fnUntraced(function* (view: Assistant.HistoryView<any> = identity) {\n yield* checkOpen\n\n const all = yield* chat.entries\n\n const read: AssistantLoop.ChatHistory = {\n messages: History.fromEntries(view(AppEvents.decodeEntries(app, all))),\n throughSeq: lastSeq(all, startSeq)\n }\n\n return read\n })\n\n const instructions = (given: string | Instructions.Resolved) =>\n mapText(given, (text) => `${Instructions.protocol}\\n\\n${text}`)\n\n // The loop's own prompt and tools, so the prompt stays cached, with the nudge at the end.\n const replyPass = <R>(options: {\n readonly instructions: Instructions.Resolved\n readonly toolkit: Toolkits.Tools<R>\n readonly view: Assistant.HistoryView<any>\n readonly nudge: string | undefined\n }) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const all = yield* chat.entries\n const through = handledThrough(all, startSeq)\n\n // Something arrived after what the loop's model read: the next turn reads it with the full loop, and replies.\n if (History.unreadAfter(all, through)) return Stream.empty\n\n const owed = History.owesReply(all, through)\n const undelivered = !owed && History.owesDelivery(all, through)\n\n if (!owed && !undelivered) return Stream.empty\n\n const nudge = undelivered ? deliveryNudge : (options.nudge ?? defaultReplyNudge)\n\n return Loop.run({\n instructions: options.instructions,\n // A reply that's owed can only be sent; a result it may have given may also be for nobody.\n toolkit: Toolkits.runOnly(\n options.toolkit,\n owed ? [Tools.sendUserMessageName] : [Tools.sendUserMessageName, Tools.waitToolName],\n \"Not now: reply to the user with send_user_message.\"\n ),\n history: Effect.gen(function* () {\n yield* checkOpen\n\n const entries = yield* chat.entries\n\n return {\n messages: [\n ...History.fromEntries(options.view(AppEvents.decodeEntries(app, entries))),\n History.userText(nudge)\n ],\n throughSeq: lastSeq(entries, startSeq)\n }\n }),\n // A second step, for a reply after a call it was refused.\n maxSteps: 2,\n // The pass runs because the model wrote text of its own instead of replying, which reaches nobody: it has to\n // call a tool now. Changing the choice costs a read of the history at full price, on these rare turns only.\n toolChoice: \"required\"\n // The loop ran the approved calls; a pass that can only reply runs none.\n }).pipe(Stream.provideService(Loop.TurnApprovals, Option.none()))\n })\n )\n\n const replyIfOwed = (options: Assistant.ReplyOptions) =>\n running(\n replyPass({\n instructions: instructions(options.instructions),\n toolkit: builtins,\n view: identity,\n nudge: options.nudge\n })\n )\n\n const prepare = <T extends Record<string, Tool.Any>>(options: Assistant.LoopOptions<T, any>) => {\n const own = Toolkits.showing(Toolkits.fromToolkitOrEmpty(options.toolkit), shownTools)\n\n return {\n system: instructions(options.instructions),\n toolkit: Toolkits.merge(builtins, own),\n view: options.history ?? identity\n }\n }\n\n return {\n chatId: chat.chatId,\n history,\n instructions,\n replyIfOwed,\n loop: (options) => {\n const { system, toolkit, view } = prepare(options)\n const twice = Toolkits.duplicateNames(toolkit)\n\n const loop =\n twice.length > 0\n ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(\", \")}`)\n : Loop.run({\n instructions: system,\n toolkit,\n history: history(view),\n maxSteps: options.maxSteps ?? 16\n })\n\n return running(Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge })))\n }\n }\n}\n\n// Every event the handler's loop emits is recorded before the loop is pulled again.\n/** @internal */\nexport const run = Effect.fnUntraced(\n function* (options: {\n readonly registration: Registration\n readonly handler: TurnHandler\n readonly chat: Writer\n readonly services: Context.Context<Assistant.TurnServices | LanguageModel.LanguageModel>\n // A chat the app deleted is deleted here, and the turn stops.\n readonly checkOpen: Effect.Effect<void>\n }) {\n const { chat } = options\n const before = yield* chat.entries\n\n yield* Telemetry.agentInput(History.unhandledInputs(before))\n\n const assistant = options.registration.definition.name\n const traceId = yield* Telemetry.currentTraceId\n\n const started = yield* chat.append([\n AssistantEvent.TurnStarted.make(\n Option.match(traceId, {\n onNone: () => ({ assistant }),\n onSome: (id) => ({ assistant, traceId: id })\n })\n )\n ])\n\n const startSeq = lastSeq(started, 0)\n const ranLoop = yield* Ref.make(false)\n const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop)\n\n const record = (event: AssistantLoop.Event) => chat.append([event])\n\n yield* options.handler(turn).pipe(\n Stream.runForEach(record),\n Effect.onExit((exit) => (Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSeq))),\n // A loop the app writes gets the assistant's own tools too, with this turn's services.\n Effect.provideService(\n Loop.TurnTools,\n Option.some(Toolkits.provide(options.registration.builtins, options.services))\n ),\n // A turn's loop waits for the user's approval, and runs the calls they approved.\n Effect.provideService(Loop.TurnApprovals, Option.some({ approved: Effect.map(chat.entries, approvedCalls) })),\n Effect.provideContext(options.services),\n Effect.matchCauseEffect({\n onSuccess: Effect.fnUntraced(function* () {\n const all = yield* chat.entries\n\n yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop))\n }),\n onFailure: (cause) => failTurn(chat, cause)\n })\n )\n\n // The trace's output is what the user saw this turn, and a failure if any.\n const shown = (yield* chat.entries).flatMap(({ event, seq }) =>\n seq > startSeq && (Predicate.isTagged(event, \"AssistantMessage\") || Predicate.isTagged(event, \"TurnFailed\"))\n ? [event]\n : []\n )\n\n yield* Telemetry.agentOutput(shown)\n },\n (effect, options) =>\n effect.pipe(\n Telemetry.agentSpan(options.registration.definition.name, {\n chatId: options.chat.chatId,\n root: true\n })\n )\n)\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/Assistant.ts
CHANGED
|
@@ -34,7 +34,6 @@ import type { CurrentChat } from "./CurrentChat.ts"
|
|
|
34
34
|
import type * as Instructions from "./Instructions.ts"
|
|
35
35
|
import * as AppEvents from "./internal/events.ts"
|
|
36
36
|
import * as Turns from "./internal/turns.ts"
|
|
37
|
-
import type { Skill } from "./Skill.ts"
|
|
38
37
|
import type * as Specialist from "./Specialist.ts"
|
|
39
38
|
|
|
40
39
|
const TypeId = "~effect-assistant/Assistant" as const
|
|
@@ -54,21 +53,16 @@ export type TurnServices = CurrentChat | Specialist.Runner
|
|
|
54
53
|
* @category models
|
|
55
54
|
* @since 0.1.0
|
|
56
55
|
*/
|
|
57
|
-
export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>,
|
|
56
|
+
export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>, Events = never> {
|
|
58
57
|
/** The assistant's role and rules. The loop places `Instructions.protocol` before them. */
|
|
59
58
|
readonly instructions: string | Instructions.Resolved
|
|
60
59
|
/**
|
|
61
60
|
* Tools the model can use in addition to the assistant's own. This is an `effect/ai` toolkit,
|
|
62
61
|
* and its handlers come from the layer. Combine several toolkits with `Toolkit.merge`, for
|
|
63
|
-
* example `Memory.Tools` or a `ToolDiscovery.make(...)` toolkit together
|
|
62
|
+
* example `Memory.Tools`, a `Skill.toolkit(...)`, or a `ToolDiscovery.make(...)` toolkit together
|
|
63
|
+
* with your own.
|
|
64
64
|
*/
|
|
65
65
|
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
66
|
-
/**
|
|
67
|
-
* Playbooks the model can follow. The prompt lists them as a catalog, and the model loads one
|
|
68
|
-
* with `load_skill` when a task matches it. A skill made with `tools` is left out when the turn's
|
|
69
|
-
* tools lack one of them.
|
|
70
|
-
*/
|
|
71
|
-
readonly skills?: ReadonlyArray<Skill<RS>> | undefined
|
|
72
66
|
/** The maximum number of model calls per turn. Defaults to 16. */
|
|
73
67
|
readonly maxSteps?: number | undefined
|
|
74
68
|
/** The message the reply pass gives the model when the turn would end without a reply. */
|
|
@@ -188,15 +182,15 @@ export interface Turn<Events = never> {
|
|
|
188
182
|
readonly instructions: (instructions: string | Instructions.Resolved) => Instructions.Resolved
|
|
189
183
|
/**
|
|
190
184
|
* Runs the agent loop over the chat. The loop uses `Instructions.protocol`, the assistant's own
|
|
191
|
-
* tools, and the instructions
|
|
185
|
+
* tools, and the instructions and tools in `options`. Every step is a tool call, and
|
|
192
186
|
* the loop ends with `replyIfOwed`.
|
|
193
187
|
*/
|
|
194
|
-
readonly loop: <T extends Record<string, Tool.Any> = Record<never, never
|
|
195
|
-
options: LoopOptions<T,
|
|
188
|
+
readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(
|
|
189
|
+
options: LoopOptions<T, Events>
|
|
196
190
|
) => Stream.Stream<
|
|
197
191
|
AssistantLoop.Event,
|
|
198
192
|
AiError.AiError,
|
|
199
|
-
LanguageModel.LanguageModel | TurnServices | AssistantLoop.ToolkitServices<T>
|
|
193
|
+
LanguageModel.LanguageModel | TurnServices | AssistantLoop.ToolkitServices<T>
|
|
200
194
|
>
|
|
201
195
|
/**
|
|
202
196
|
* Runs one more pass when the turn would otherwise end without a reply to the user's latest
|
package/src/AssistantEvent.ts
CHANGED
|
@@ -140,13 +140,30 @@ export class SpecialistFinished extends Schema.TaggedClass<SpecialistFinished>()
|
|
|
140
140
|
error: Schema.optional(Schema.String)
|
|
141
141
|
}) {}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Represents the user's decision on a tool call that waits for their approval: `"approved"`, so
|
|
145
|
+
* the call runs, or `"rejected"`, with an optional short reason the model reads.
|
|
146
|
+
*
|
|
147
|
+
* Its id is fixed per call (see `ToolApproval.approve`), so a decision is stored once. A decision on
|
|
148
|
+
* a call that isn't waiting any more (decided, or expired) isn't stored.
|
|
149
|
+
*
|
|
150
|
+
* @category schemas
|
|
151
|
+
* @since 0.4.0
|
|
152
|
+
*/
|
|
153
|
+
export class ApprovalDecision extends Schema.TaggedClass<ApprovalDecision>()("ApprovalDecision", {
|
|
154
|
+
id: InputId,
|
|
155
|
+
callId: Schema.String,
|
|
156
|
+
decision: Schema.Literals(["approved", "rejected"]),
|
|
157
|
+
reason: Schema.optional(Schema.String.check(Schema.isMaxLength(500)))
|
|
158
|
+
}) {}
|
|
159
|
+
|
|
143
160
|
/**
|
|
144
161
|
* Schema for every input `Assistant.submit` accepts.
|
|
145
162
|
*
|
|
146
163
|
* @category schemas
|
|
147
164
|
* @since 0.1.0
|
|
148
165
|
*/
|
|
149
|
-
export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished])
|
|
166
|
+
export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished, ApprovalDecision])
|
|
150
167
|
|
|
151
168
|
/**
|
|
152
169
|
* Represents an input to an assistant.
|
|
@@ -157,7 +174,8 @@ export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished]
|
|
|
157
174
|
export type Input = typeof Input.Type
|
|
158
175
|
|
|
159
176
|
/**
|
|
160
|
-
* Schema for the inputs from the chat's user, which are messages and
|
|
177
|
+
* Schema for the inputs from the chat's user, which are messages, block answers, and decisions on
|
|
178
|
+
* tool calls that wait for their approval.
|
|
161
179
|
*
|
|
162
180
|
* A turn owes each one a reply. It is also what a chat client usually may submit, since the app's
|
|
163
181
|
* events come from the server.
|
|
@@ -165,7 +183,7 @@ export type Input = typeof Input.Type
|
|
|
165
183
|
* @category schemas
|
|
166
184
|
* @since 0.1.0
|
|
167
185
|
*/
|
|
168
|
-
export const UserInput = Schema.Union([UserMessage, BlockAnswer])
|
|
186
|
+
export const UserInput = Schema.Union([UserMessage, BlockAnswer, ApprovalDecision])
|
|
169
187
|
|
|
170
188
|
/**
|
|
171
189
|
* Represents an input from the chat's user.
|
|
@@ -179,12 +197,16 @@ export type UserInput = typeof UserInput.Type
|
|
|
179
197
|
* Represents the UI shown with a message, as a block kind from the assistant's `BlockKit` and its
|
|
180
198
|
* props.
|
|
181
199
|
*
|
|
200
|
+
* A block that shows a tool's result (`Block.fromToolResult`) has the tool's name as its kind and
|
|
201
|
+
* the result's `resultId`.
|
|
202
|
+
*
|
|
182
203
|
* @category schemas
|
|
183
204
|
* @since 0.1.0
|
|
184
205
|
*/
|
|
185
206
|
export class MessageBlock extends Schema.Class<MessageBlock>("effect-assistant/MessageBlock")({
|
|
186
207
|
kind: Schema.String,
|
|
187
|
-
props: Schema.Unknown
|
|
208
|
+
props: Schema.Unknown,
|
|
209
|
+
resultId: Schema.optional(Schema.String)
|
|
188
210
|
}) {}
|
|
189
211
|
|
|
190
212
|
/**
|
|
@@ -307,6 +329,10 @@ export type ToolOutcome = typeof ToolOutcome.Type
|
|
|
307
329
|
* failure includes its reason when it has one, which is a `ToolFailure`'s `reason` or the
|
|
308
330
|
* failure's tag.
|
|
309
331
|
*
|
|
332
|
+
* When the model reads less than the whole success (a `ToolResult.modelView`, or the `resultId`
|
|
333
|
+
* added for a tool the assistant's blocks show), `full` holds the whole success, encoded by the
|
|
334
|
+
* tool's success schema.
|
|
335
|
+
*
|
|
310
336
|
* @category schemas
|
|
311
337
|
* @since 0.1.0
|
|
312
338
|
*/
|
|
@@ -315,7 +341,10 @@ export class ToolResult extends Schema.TaggedClass<ToolResult>()("ToolResult", {
|
|
|
315
341
|
tool: Schema.String,
|
|
316
342
|
outcome: ToolOutcome,
|
|
317
343
|
result: Schema.Json,
|
|
318
|
-
reason: Schema.optional(Schema.String)
|
|
344
|
+
reason: Schema.optional(Schema.String),
|
|
345
|
+
full: Schema.optional(Schema.Json),
|
|
346
|
+
/** The id the model passes in a `tool_result` block to show this result. */
|
|
347
|
+
resultId: Schema.optional(Schema.String)
|
|
319
348
|
}) {}
|
|
320
349
|
|
|
321
350
|
/**
|
|
@@ -400,6 +429,36 @@ export class BlocksClosed extends Schema.TaggedClass<BlocksClosed>()("BlocksClos
|
|
|
400
429
|
messageIds: Schema.Array(Schema.String)
|
|
401
430
|
}) {}
|
|
402
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Represents a tool call that waits for the user's approval instead of running: its tool needs
|
|
434
|
+
* approval (Effect's `needsApproval`).
|
|
435
|
+
*
|
|
436
|
+
* **Details**
|
|
437
|
+
*
|
|
438
|
+
* It holds the call's id, the tool, and its params, in JSON, as the model wrote them. The model
|
|
439
|
+
* reads it as the call's result, saying it waits for the user; the user's
|
|
440
|
+
* {@link ApprovalDecision} decides it, and an approved call runs then, with the same id.
|
|
441
|
+
*
|
|
442
|
+
* @category schemas
|
|
443
|
+
* @since 0.4.0
|
|
444
|
+
*/
|
|
445
|
+
export class ApprovalRequested extends Schema.TaggedClass<ApprovalRequested>()("ApprovalRequested", {
|
|
446
|
+
callId: Schema.String,
|
|
447
|
+
tool: Schema.String,
|
|
448
|
+
params: Schema.Json
|
|
449
|
+
}) {}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Represents tool calls that waited for the user's approval and won't run: the user moved on, like
|
|
453
|
+
* the ones `ToolApproval.closeStale` expires. The model reads that they weren't approved.
|
|
454
|
+
*
|
|
455
|
+
* @category schemas
|
|
456
|
+
* @since 0.4.0
|
|
457
|
+
*/
|
|
458
|
+
export class ApprovalsExpired extends Schema.TaggedClass<ApprovalsExpired>()("ApprovalsExpired", {
|
|
459
|
+
callIds: Schema.Array(Schema.String)
|
|
460
|
+
}) {}
|
|
461
|
+
|
|
403
462
|
/**
|
|
404
463
|
* Schema for every event the library defines.
|
|
405
464
|
*
|
|
@@ -419,6 +478,9 @@ export const AssistantEvent = Schema.Union([
|
|
|
419
478
|
SpecialistStarted,
|
|
420
479
|
SpecialistFinished,
|
|
421
480
|
BlocksClosed,
|
|
481
|
+
ApprovalRequested,
|
|
482
|
+
ApprovalDecision,
|
|
483
|
+
ApprovalsExpired,
|
|
422
484
|
ChatDeleted
|
|
423
485
|
])
|
|
424
486
|
|
|
@@ -554,7 +616,7 @@ export type Entry = typeof Entry.Type
|
|
|
554
616
|
|
|
555
617
|
/**
|
|
556
618
|
* Returns whether an event is one of the library's inputs, which are a user message, a block
|
|
557
|
-
* answer,
|
|
619
|
+
* answer, a finished specialist run, or a decision on a tool call waiting for approval.
|
|
558
620
|
*
|
|
559
621
|
* The application's events that came in through the assistant's `submit` are inputs too (see
|
|
560
622
|
* {@link isAppInput}).
|
|
@@ -565,7 +627,8 @@ export type Entry = typeof Entry.Type
|
|
|
565
627
|
export const isInput = (event: { readonly _tag: string }): event is Input =>
|
|
566
628
|
Predicate.isTagged(event, "UserMessage") ||
|
|
567
629
|
Predicate.isTagged(event, "BlockAnswer") ||
|
|
568
|
-
Predicate.isTagged(event, "SpecialistFinished")
|
|
630
|
+
Predicate.isTagged(event, "SpecialistFinished") ||
|
|
631
|
+
Predicate.isTagged(event, "ApprovalDecision")
|
|
569
632
|
|
|
570
633
|
/**
|
|
571
634
|
* Returns whether an input starts a turn, which every input does except a run the assistant
|
|
@@ -584,7 +647,9 @@ export const isWaking = (input: Input): boolean =>
|
|
|
584
647
|
* @since 0.1.0
|
|
585
648
|
*/
|
|
586
649
|
export const isUserInput = (input: Input): input is UserInput =>
|
|
587
|
-
Predicate.isTagged(input, "UserMessage") ||
|
|
650
|
+
Predicate.isTagged(input, "UserMessage") ||
|
|
651
|
+
Predicate.isTagged(input, "BlockAnswer") ||
|
|
652
|
+
Predicate.isTagged(input, "ApprovalDecision")
|
|
588
653
|
|
|
589
654
|
/**
|
|
590
655
|
* Returns whether an event ends a turn, and so says how far the assistant has read.
|
package/src/AssistantLoop.ts
CHANGED
|
@@ -161,8 +161,10 @@ const toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>
|
|
|
161
161
|
* the step's result and decides what comes next, with `AssistantLoop.next(state)` or
|
|
162
162
|
* `AssistantLoop.stop()`.
|
|
163
163
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
164
|
+
* In a turn's loop, a call to a tool with Effect's `needsApproval` doesn't run: the step emits an
|
|
165
|
+
* `ApprovalRequested` for it, and ends the turn, which waits for the user (see `ToolApproval`). A
|
|
166
|
+
* call the user approved runs at the start of the next step, before the model is called. A
|
|
167
|
+
* specialist's run can't wait for the user: such a call fails, and the run reads why.
|
|
166
168
|
*
|
|
167
169
|
* @category constructors
|
|
168
170
|
* @since 0.1.0
|
package/src/Block.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* pick from, a yes or no, or a small input. The assistant fills in its `props`, and
|
|
6
6
|
* using the block sends back a value typed by its `answer` schema.
|
|
7
7
|
*
|
|
8
|
+
* A block made with {@link fromToolResult} shows a tool's result instead: the assistant picks the
|
|
9
|
+
* result, and the client draws it from what the tool returned.
|
|
10
|
+
*
|
|
8
11
|
* @since 0.1.0
|
|
9
12
|
*/
|
|
10
|
-
import type * as
|
|
13
|
+
import type * as Tool from "effect/ai/Tool"
|
|
14
|
+
import * as Predicate from "effect/Predicate"
|
|
15
|
+
import * as Schema from "effect/Schema"
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Schema that a block's props or answer can use.
|
|
@@ -82,3 +87,79 @@ export const make = <const Kind extends string, Props extends Codec, Answer exte
|
|
|
82
87
|
props: options.props,
|
|
83
88
|
answer: options.answer
|
|
84
89
|
})
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Schema for the props of a block that shows a tool's result: none, since the result is the block.
|
|
93
|
+
*
|
|
94
|
+
* @category schemas
|
|
95
|
+
* @since 0.5.0
|
|
96
|
+
*/
|
|
97
|
+
export const NoProps = Schema.Struct({})
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Represents a block that shows a tool's results, made with {@link fromToolResult}. Its kind is the
|
|
101
|
+
* tool's name.
|
|
102
|
+
*
|
|
103
|
+
* @category models
|
|
104
|
+
* @since 0.5.0
|
|
105
|
+
*/
|
|
106
|
+
export interface ToolResultBlock<T extends Tool.Any = Tool.Any> extends Block<
|
|
107
|
+
Tool.Name<T>,
|
|
108
|
+
typeof NoProps,
|
|
109
|
+
Schema.Never
|
|
110
|
+
> {
|
|
111
|
+
readonly tool: T
|
|
112
|
+
/** The tool's success schema, which the client decodes the result with. */
|
|
113
|
+
readonly result: Codec
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a block that shows a tool's results.
|
|
118
|
+
*
|
|
119
|
+
* **Details**
|
|
120
|
+
*
|
|
121
|
+
* Put it in the assistant's `BlockKit`. The tool's results then carry a `resultId` in what the model
|
|
122
|
+
* reads, and the model shows one with a `tool_result` block that names it: it never sees the
|
|
123
|
+
* block's kind or how the client draws it. Sending the block fails back to the model when no
|
|
124
|
+
* successful result of a shown tool has that id.
|
|
125
|
+
*
|
|
126
|
+
* On the client, `BlockKit.toClient` gives the block's renderer the result, decoded by the tool's
|
|
127
|
+
* success schema. The tool's definition is shared with the client for that, so it can't need
|
|
128
|
+
* server-only services: add those on the server with `Tool.addDependency`.
|
|
129
|
+
*
|
|
130
|
+
* **Example** (Showing search results as cards)
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { Block, BlockKit } from "effect-assistant"
|
|
134
|
+
* import { SearchAssets } from "./tools.ts"
|
|
135
|
+
*
|
|
136
|
+
* export const Blocks = BlockKit.make([PickOne, Block.fromToolResult(SearchAssets)])
|
|
137
|
+
*
|
|
138
|
+
* // Client
|
|
139
|
+
* const renderBlock = BlockKit.toClient<typeof Blocks, ReactNode>(Blocks, {
|
|
140
|
+
* pick_one: ({ props, respond }) => <Options options={props.options} onPick={respond} />,
|
|
141
|
+
* search_assets: ({ result }) => <AssetCards assets={result.items} />
|
|
142
|
+
* })
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @category constructors
|
|
146
|
+
* @since 0.5.0
|
|
147
|
+
*/
|
|
148
|
+
export const fromToolResult = <T extends Tool.Any>(
|
|
149
|
+
tool: T & { readonly successSchema: Codec }
|
|
150
|
+
): ToolResultBlock<T> => ({
|
|
151
|
+
kind: tool.name,
|
|
152
|
+
description: `Shows a ${tool.name} result.`,
|
|
153
|
+
props: NoProps,
|
|
154
|
+
answer: undefined,
|
|
155
|
+
tool,
|
|
156
|
+
result: tool.successSchema
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns whether a block shows a tool's results.
|
|
161
|
+
*
|
|
162
|
+
* @category guards
|
|
163
|
+
* @since 0.5.0
|
|
164
|
+
*/
|
|
165
|
+
export const isToolResult = (block: Any): block is ToolResultBlock => Predicate.hasProperty(block, "tool")
|
package/src/BlockKit.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @since 0.1.0
|
|
10
10
|
*/
|
|
11
|
+
import type * as Tool from "effect/ai/Tool"
|
|
11
12
|
import * as Effect from "effect/Effect"
|
|
12
13
|
import * as Option from "effect/Option"
|
|
13
14
|
import * as Predicate from "effect/Predicate"
|
|
14
15
|
import * as Schema from "effect/Schema"
|
|
15
16
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
16
|
-
import
|
|
17
|
+
import * as Block from "./Block.ts"
|
|
17
18
|
import type * as Timeline from "./Timeline.ts"
|
|
18
19
|
import { CurrentChat } from "./CurrentChat.ts"
|
|
19
20
|
import * as InputId from "./InputId.ts"
|
|
@@ -40,6 +41,15 @@ export const make = <const Blocks extends ReadonlyArray<Block.Any>>(blocks: Bloc
|
|
|
40
41
|
byKind: new Map(blocks.map((block) => [block.kind, block]))
|
|
41
42
|
})
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Returns the names of the tools whose results the kit's blocks show (`Block.fromToolResult`).
|
|
46
|
+
*
|
|
47
|
+
* @category utils
|
|
48
|
+
* @since 0.5.0
|
|
49
|
+
*/
|
|
50
|
+
export const toolNames = (kit: BlockKit | undefined): ReadonlySet<string> =>
|
|
51
|
+
new Set((kit?.blocks ?? []).flatMap((block) => (Block.isToolResult(block) ? [block.tool.name] : [])))
|
|
52
|
+
|
|
43
53
|
type KindOf<B> = B extends { readonly kind: infer K extends string } ? K : never
|
|
44
54
|
|
|
45
55
|
type BlockOf<Kit extends BlockKit, K> = Extract<Kit["blocks"][number], { readonly kind: K }>
|
|
@@ -48,6 +58,13 @@ type PropsOf<B> = B extends { readonly props: Schema.Top } ? B["props"]["Type"]
|
|
|
48
58
|
|
|
49
59
|
type AnswerOf<B> = B extends { readonly answer: infer A } ? (A extends Schema.Top ? A["Type"] : never) : never
|
|
50
60
|
|
|
61
|
+
type ResultArgs<B> = B extends { readonly tool: infer T }
|
|
62
|
+
? {
|
|
63
|
+
/** The tool's result the block shows, decoded by its success schema. */
|
|
64
|
+
readonly result: Tool.Success<T>
|
|
65
|
+
}
|
|
66
|
+
: unknown
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
69
|
* Represents the state of a message's block.
|
|
53
70
|
*
|
|
@@ -164,12 +181,21 @@ export const closeStale = Effect.fn(function* (options: { readonly messages: num
|
|
|
164
181
|
})
|
|
165
182
|
|
|
166
183
|
/**
|
|
167
|
-
* Represents what a renderer receives for one block.
|
|
184
|
+
* Represents what a renderer receives for one block. A block that shows a tool's result also gets
|
|
185
|
+
* the `result`.
|
|
168
186
|
*
|
|
169
187
|
* @category models
|
|
170
188
|
* @since 0.1.0
|
|
171
189
|
*/
|
|
172
|
-
export
|
|
190
|
+
export type RenderArgs<B> = BlockArgs<B> & ResultArgs<B>
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Represents what every block's renderer receives.
|
|
194
|
+
*
|
|
195
|
+
* @category models
|
|
196
|
+
* @since 0.5.0
|
|
197
|
+
*/
|
|
198
|
+
export interface BlockArgs<B> {
|
|
173
199
|
/**
|
|
174
200
|
* The id of the message the block is on. A block whose button calls your API can key the work
|
|
175
201
|
* on it, with the chat's id, so the work runs once per block.
|
|
@@ -256,7 +282,7 @@ export function toClient<Kit extends BlockKit, Out>(
|
|
|
256
282
|
// arguments.
|
|
257
283
|
export function toClient<Out>(
|
|
258
284
|
kit: BlockKit,
|
|
259
|
-
renderers: Readonly<Record<string, (args:
|
|
285
|
+
renderers: Readonly<Record<string, (args: BlockArgs<Block.Any> & { readonly result?: unknown }) => Out>>
|
|
260
286
|
): (message: Timeline.AssistantMessage, options: RenderOptions) => Out | undefined {
|
|
261
287
|
return (message, options) => {
|
|
262
288
|
if (message.block === undefined) return undefined
|
|
@@ -271,6 +297,12 @@ export function toClient<Out>(
|
|
|
271
297
|
|
|
272
298
|
if (Option.isNone(props)) return undefined
|
|
273
299
|
|
|
300
|
+
const result = Block.isToolResult(block)
|
|
301
|
+
? Schema.decodeUnknownOption(Schema.toCodecJson(block.result))(message.block.result)
|
|
302
|
+
: Option.some(undefined)
|
|
303
|
+
|
|
304
|
+
if (Option.isNone(result)) return undefined
|
|
305
|
+
|
|
274
306
|
const given = message.block.answer
|
|
275
307
|
const answerCodec = block.answer === undefined ? undefined : Schema.toCodecJson(block.answer)
|
|
276
308
|
|
|
@@ -281,6 +313,7 @@ export function toClient<Out>(
|
|
|
281
313
|
messageId: message.id,
|
|
282
314
|
text: message.text,
|
|
283
315
|
props: props.value,
|
|
316
|
+
result: result.value,
|
|
284
317
|
open: message.block.status === "open",
|
|
285
318
|
answer: Option.getOrUndefined(answer),
|
|
286
319
|
respond: (value) =>
|