effect-assistant 0.5.0 → 0.7.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 +10 -10
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +2 -2
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +5 -5
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +3 -3
- package/dist/AssistantEngine.js +2 -2
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +8 -8
- package/dist/AssistantEvent.js +7 -7
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +2 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/ChatLog.d.ts +9 -9
- package/dist/ChatLog.d.ts.map +1 -1
- package/dist/ChatLog.js +21 -21
- package/dist/ChatLog.js.map +1 -1
- package/dist/Compaction.d.ts +10 -10
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +16 -16
- package/dist/Compaction.js.map +1 -1
- package/dist/Memory.d.ts +1 -1
- package/dist/Memory.js +1 -1
- package/dist/Memory.js.map +1 -1
- package/dist/Skill.d.ts +2 -2
- package/dist/Skill.js +2 -2
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +17 -16
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js +5 -5
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +9 -9
- package/dist/Timeline.js +16 -16
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +1 -1
- package/dist/ToolApproval.js +1 -1
- package/dist/ToolApproval.js.map +1 -1
- package/dist/ToolDiscovery.d.ts +1 -1
- package/dist/ToolDiscovery.js +1 -1
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/internal/chat.js +4 -4
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.js +1 -1
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +23 -23
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +1 -1
- package/dist/internal/loop.js +4 -4
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +6 -6
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +18 -18
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +11 -11
- package/src/AssistantActivity.ts +9 -7
- package/src/AssistantEngine.ts +3 -3
- package/src/AssistantEvent.ts +8 -8
- package/src/AssistantLoop.ts +3 -3
- package/src/ChatLog.ts +31 -26
- package/src/Compaction.ts +21 -21
- package/src/Memory.ts +1 -1
- package/src/Skill.ts +2 -2
- package/src/Specialist.ts +19 -18
- package/src/Timeline.ts +22 -22
- package/src/ToolApproval.ts +1 -1
- package/src/ToolDiscovery.ts +1 -1
- package/src/internal/chat.ts +4 -4
- package/src/internal/events.ts +1 -1
- package/src/internal/history.ts +32 -28
- package/src/internal/loop.ts +5 -5
- package/src/internal/specialists.ts +7 -7
- package/src/internal/turns.ts +28 -26
package/dist/internal/turns.js
CHANGED
|
@@ -63,9 +63,9 @@ const approvedCalls = (entries) => [...fromEntries(entries).values()].flatMap(({
|
|
|
63
63
|
params: request.params,
|
|
64
64
|
providerExecuted: false
|
|
65
65
|
})] : []);
|
|
66
|
-
const
|
|
67
|
-
const closeOpenCalls = Effect.fnUntraced(function* (chat,
|
|
68
|
-
const entries = (yield* chat.entries).filter(({
|
|
66
|
+
const lastSequence = (entries, otherwise) => entries.at(-1)?.sequence ?? otherwise;
|
|
67
|
+
const closeOpenCalls = Effect.fnUntraced(function* (chat, startSequence) {
|
|
68
|
+
const entries = (yield* chat.entries).filter(({ sequence }) => sequence > startSequence);
|
|
69
69
|
const started = new Set(entries.flatMap(({ event }) => Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []));
|
|
70
70
|
const open = unansweredCalls(fromEntries$1(entries));
|
|
71
71
|
if (open.length === 0) return;
|
|
@@ -82,24 +82,24 @@ const closeOpenCalls = Effect.fnUntraced(function* (chat, startSeq) {
|
|
|
82
82
|
}
|
|
83
83
|
})));
|
|
84
84
|
});
|
|
85
|
-
const handledThrough = (entries,
|
|
85
|
+
const handledThrough = (entries, startSequence) => entries.reduce((through, { sequence, event }) => sequence > startSequence && isLibraryEvent(event) && Predicate.isTagged(event, "ModelOutput") && event.seenThroughSequence !== void 0 ? Math.max(through, event.seenThroughSequence) : through, startSequence);
|
|
86
86
|
const finishTurn = Effect.fnUntraced(function* (chat, read, ranLoop) {
|
|
87
87
|
const entries = yield* chat.entries;
|
|
88
88
|
const end = ranLoop && owesReply(entries, read) && !unreadAfter(entries, read) ? TurnFailed.make({
|
|
89
|
-
|
|
89
|
+
throughSequence: read,
|
|
90
90
|
message: silentTurnMessage
|
|
91
|
-
}) : TurnCompleted.make({
|
|
91
|
+
}) : TurnCompleted.make({ throughSequence: read });
|
|
92
92
|
yield* chat.append([end]);
|
|
93
93
|
});
|
|
94
94
|
const failTurn = Effect.fnUntraced(function* (chat, cause) {
|
|
95
95
|
yield* Effect.logError("assistant turn failed", cause);
|
|
96
|
-
const last =
|
|
96
|
+
const last = lastSequence(yield* chat.entries, 0);
|
|
97
97
|
yield* chat.append([TurnFailed.make({
|
|
98
|
-
|
|
98
|
+
throughSequence: last,
|
|
99
99
|
message: failureMessage(cause)
|
|
100
100
|
})]);
|
|
101
101
|
});
|
|
102
|
-
const makeTurnApi = (registration, chat,
|
|
102
|
+
const makeTurnApi = (registration, chat, startSequence, checkOpen, ranLoop) => {
|
|
103
103
|
const { definition, builtins } = registration;
|
|
104
104
|
const { app } = definition;
|
|
105
105
|
const shownTools = toolNames(definition.blocks);
|
|
@@ -109,13 +109,13 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
|
109
109
|
const all = yield* chat.entries;
|
|
110
110
|
return {
|
|
111
111
|
messages: fromEntries$1(view(decodeEntries(app, all))),
|
|
112
|
-
|
|
112
|
+
throughSequence: lastSequence(all, startSequence)
|
|
113
113
|
};
|
|
114
114
|
});
|
|
115
115
|
const instructions = (given) => mapText(given, (text) => `${protocol}\n\n${text}`);
|
|
116
116
|
const replyPass = (options) => Stream.unwrap(Effect.gen(function* () {
|
|
117
117
|
const all = yield* chat.entries;
|
|
118
|
-
const through = handledThrough(all,
|
|
118
|
+
const through = handledThrough(all, startSequence);
|
|
119
119
|
if (unreadAfter(all, through)) return Stream.empty;
|
|
120
120
|
const owed = owesReply(all, through);
|
|
121
121
|
const undelivered = !owed && owesDelivery(all, through);
|
|
@@ -129,7 +129,7 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
|
129
129
|
const entries = yield* chat.entries;
|
|
130
130
|
return {
|
|
131
131
|
messages: [...fromEntries$1(options.view(decodeEntries(app, entries))), userText(nudge)],
|
|
132
|
-
|
|
132
|
+
throughSequence: lastSequence(entries, startSequence)
|
|
133
133
|
};
|
|
134
134
|
}),
|
|
135
135
|
maxSteps: 2,
|
|
@@ -155,7 +155,7 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
|
155
155
|
history,
|
|
156
156
|
instructions,
|
|
157
157
|
replyIfOwed,
|
|
158
|
-
|
|
158
|
+
run: (options) => {
|
|
159
159
|
const { system, toolkit, view } = prepare(options);
|
|
160
160
|
const twice = duplicateNames(toolkit);
|
|
161
161
|
const loop = twice.length > 0 ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(", ")}`) : run$1({
|
|
@@ -187,18 +187,18 @@ const run = Effect.fnUntraced(function* (options) {
|
|
|
187
187
|
traceId: id
|
|
188
188
|
})
|
|
189
189
|
}))]);
|
|
190
|
-
const
|
|
190
|
+
const startSequence = lastSequence(started, 0);
|
|
191
191
|
const ranLoop = yield* Ref.make(false);
|
|
192
|
-
const turn = makeTurnApi(options.registration, chat,
|
|
192
|
+
const turn = makeTurnApi(options.registration, chat, startSequence, options.checkOpen, ranLoop);
|
|
193
193
|
const record = (event) => chat.append([event]);
|
|
194
|
-
yield* options.handler(turn).pipe(Stream.runForEach(record), Effect.onExit((exit) => Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat,
|
|
194
|
+
yield* options.handler(turn).pipe(Stream.runForEach(record), Effect.onExit((exit) => Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSequence)), 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({
|
|
195
195
|
onSuccess: Effect.fnUntraced(function* () {
|
|
196
196
|
const all = yield* chat.entries;
|
|
197
|
-
yield* finishTurn(chat, handledThrough(all,
|
|
197
|
+
yield* finishTurn(chat, handledThrough(all, startSequence), yield* Ref.get(ranLoop));
|
|
198
198
|
}),
|
|
199
199
|
onFailure: (cause) => failTurn(chat, cause)
|
|
200
200
|
}));
|
|
201
|
-
const shown = (yield* chat.entries).flatMap(({ event,
|
|
201
|
+
const shown = (yield* chat.entries).flatMap(({ event, sequence }) => sequence > startSequence && (Predicate.isTagged(event, "AssistantMessage") || Predicate.isTagged(event, "TurnFailed")) ? [event] : []);
|
|
202
202
|
yield* agentOutput(shown);
|
|
203
203
|
}, (effect, options) => effect.pipe(agentSpan(options.registration.definition.name, {
|
|
204
204
|
chatId: options.chat.chatId,
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 lastSequence = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) =>\n entries.at(-1)?.sequence ?? 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, startSequence: number) {\n const entries = (yield* chat.entries).filter(({ sequence }) => sequence > startSequence)\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 `startSequence`) woke it; past that, what its model outputs record\n// reading.\nconst handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSequence: number) =>\n entries.reduce(\n (through, { sequence, event }) =>\n sequence > startSequence &&\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ModelOutput\") &&\n event.seenThroughSequence !== undefined\n ? Math.max(through, event.seenThroughSequence)\n : through,\n startSequence\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({ throughSequence: read, message: silentTurnMessage })\n : AssistantEvent.TurnCompleted.make({ throughSequence: 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 = lastSequence(yield* chat.entries, 0)\n\n yield* chat.append([AssistantEvent.TurnFailed.make({ throughSequence: last, message: failureMessage(cause) })])\n})\n\nconst makeTurnApi = (\n registration: Registration,\n chat: Writer,\n startSequence: 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 throughSequence: lastSequence(all, startSequence)\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, startSequence)\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 throughSequence: lastSequence(entries, startSequence)\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.RunOptions<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 run: (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 startSequence = lastSequence(started, 0)\n const ranLoop = yield* Ref.make(false)\n const turn = makeTurnApi(options.registration, chat, startSequence, 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, startSequence))),\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, startSequence), 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, sequence }) =>\n sequence > startSequence &&\n (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
|
@@ -48,12 +48,12 @@ const TypeId = "~effect-assistant/Assistant" as const
|
|
|
48
48
|
export type TurnServices = CurrentChat | Specialist.Runner
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Represents the options accepted by `turn.
|
|
51
|
+
* Represents the options accepted by `turn.run`.
|
|
52
52
|
*
|
|
53
53
|
* @category models
|
|
54
54
|
* @since 0.1.0
|
|
55
55
|
*/
|
|
56
|
-
export interface
|
|
56
|
+
export interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>, Events = never> {
|
|
57
57
|
/** The assistant's role and rules. The loop places `Instructions.protocol` before them. */
|
|
58
58
|
readonly instructions: string | Instructions.Resolved
|
|
59
59
|
/**
|
|
@@ -115,7 +115,7 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
115
115
|
*
|
|
116
116
|
* // An `OrderPlaced` that the API submits wakes a turn, which reads it as this note.
|
|
117
117
|
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
118
|
-
* turn.
|
|
118
|
+
* turn.run({ instructions: "Be brief.", history: withOrders })
|
|
119
119
|
* )
|
|
120
120
|
* ```
|
|
121
121
|
*
|
|
@@ -134,7 +134,7 @@ export type HistoryView<Events = never> = (
|
|
|
134
134
|
*/
|
|
135
135
|
export interface Entry<Events = never> {
|
|
136
136
|
readonly chatId: ChatId
|
|
137
|
-
readonly
|
|
137
|
+
readonly sequence: number
|
|
138
138
|
readonly at: number
|
|
139
139
|
readonly event: AssistantEvent.AssistantEvent | Events
|
|
140
140
|
}
|
|
@@ -146,7 +146,7 @@ export interface Entry<Events = never> {
|
|
|
146
146
|
* @since 0.1.0
|
|
147
147
|
*/
|
|
148
148
|
export interface ReplyOptions {
|
|
149
|
-
/** The assistant's instructions, as passed to `turn.
|
|
149
|
+
/** The assistant's instructions, as passed to `turn.run`. `Instructions.protocol` is added. */
|
|
150
150
|
readonly instructions: string | Instructions.Resolved
|
|
151
151
|
/**
|
|
152
152
|
* The message given to the model after the history. Defaults to a message saying that it
|
|
@@ -185,8 +185,8 @@ export interface Turn<Events = never> {
|
|
|
185
185
|
* tools, and the instructions and tools in `options`. Every step is a tool call, and
|
|
186
186
|
* the loop ends with `replyIfOwed`.
|
|
187
187
|
*/
|
|
188
|
-
readonly
|
|
189
|
-
options:
|
|
188
|
+
readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(
|
|
189
|
+
options: RunOptions<T, Events>
|
|
190
190
|
) => Stream.Stream<
|
|
191
191
|
AssistantLoop.Event,
|
|
192
192
|
AiError.AiError,
|
|
@@ -197,7 +197,7 @@ export interface Turn<Events = never> {
|
|
|
197
197
|
* message. In this pass the model can only reply.
|
|
198
198
|
*
|
|
199
199
|
* The stream is empty when no reply is owed, or when an input arrived that the next turn will
|
|
200
|
-
* read. `
|
|
200
|
+
* read. `run` runs this pass with its own prompt and tools, so the pass reuses the cached
|
|
201
201
|
* prompt.
|
|
202
202
|
*/
|
|
203
203
|
readonly replyIfOwed: (
|
|
@@ -309,7 +309,7 @@ export interface Service<Events = never> {
|
|
|
309
309
|
/**
|
|
310
310
|
* Streams the chat's stored entries, then live ones, with the assistant's own events decoded.
|
|
311
311
|
* A reader that already has the earlier entries passes `after` and receives only the entries
|
|
312
|
-
* after that `
|
|
312
|
+
* after that `sequence`.
|
|
313
313
|
*
|
|
314
314
|
* For a chat client, pipe the whole log into `Timeline.changes(MyAssistant, { after })`, which
|
|
315
315
|
* streams the chat's timeline, as its user sees it. Its `after` goes there, not here: a message
|
|
@@ -382,7 +382,7 @@ export interface Assistant<
|
|
|
382
382
|
* export const MyAssistant = Assistant.make("my-assistant")
|
|
383
383
|
*
|
|
384
384
|
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
385
|
-
* turn.
|
|
385
|
+
* turn.run({ instructions: "You help a small team. Be brief." })
|
|
386
386
|
* )
|
|
387
387
|
*
|
|
388
388
|
* const AppLive = MyAssistantLive.pipe(
|
|
@@ -458,7 +458,7 @@ function entrySchema(events: ReadonlyArray<AssistantEvent.AppEventSchema>): Sche
|
|
|
458
458
|
return Schema.toCodecJson(
|
|
459
459
|
Schema.Struct({
|
|
460
460
|
chatId: ChatId,
|
|
461
|
-
|
|
461
|
+
sequence: Schema.Int,
|
|
462
462
|
at: Schema.Finite,
|
|
463
463
|
event: Schema.Union([AssistantEvent.AssistantEvent, ...events])
|
|
464
464
|
})
|
package/src/AssistantActivity.ts
CHANGED
|
@@ -43,8 +43,8 @@ export interface State {
|
|
|
43
43
|
* to start.
|
|
44
44
|
*/
|
|
45
45
|
readonly queued: boolean
|
|
46
|
-
/** The `
|
|
47
|
-
readonly
|
|
46
|
+
/** The `sequence` of the newest input. */
|
|
47
|
+
readonly lastInputSequence: number
|
|
48
48
|
/** Specialist runs that are working, by run id. */
|
|
49
49
|
readonly specialists: ReadonlyMap<RunId, SpecialistRun>
|
|
50
50
|
}
|
|
@@ -58,7 +58,7 @@ export interface State {
|
|
|
58
58
|
export const empty: State = {
|
|
59
59
|
turnSince: undefined,
|
|
60
60
|
queued: false,
|
|
61
|
-
|
|
61
|
+
lastInputSequence: 0,
|
|
62
62
|
specialists: new Map()
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -89,14 +89,16 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State =>
|
|
|
89
89
|
const event = entry.event
|
|
90
90
|
|
|
91
91
|
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
92
|
-
return AssistantEvent.isAppInput(event)
|
|
92
|
+
return AssistantEvent.isAppInput(event)
|
|
93
|
+
? { ...activity, queued: true, lastInputSequence: entry.sequence }
|
|
94
|
+
: activity
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
if (AssistantEvent.isInput(event)) {
|
|
96
98
|
// A finished run's report is an input, and ends the run.
|
|
97
99
|
const current = Predicate.isTagged(event, "SpecialistFinished") ? withoutRun(activity, event.runId) : activity
|
|
98
100
|
|
|
99
|
-
return AssistantEvent.isWaking(event) ? { ...current, queued: true,
|
|
101
|
+
return AssistantEvent.isWaking(event) ? { ...current, queued: true, lastInputSequence: entry.sequence } : current
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
return Match.value(event).pipe(
|
|
@@ -105,7 +107,7 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State =>
|
|
|
105
107
|
Match.tag("TurnCompleted", "TurnFailed", (ended): State => ({
|
|
106
108
|
...activity,
|
|
107
109
|
turnSince: undefined,
|
|
108
|
-
queued: activity.
|
|
110
|
+
queued: activity.lastInputSequence > ended.throughSequence
|
|
109
111
|
})),
|
|
110
112
|
Match.tag("SpecialistStarted", (started): State => ({
|
|
111
113
|
...activity,
|
|
@@ -117,7 +119,7 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State =>
|
|
|
117
119
|
})
|
|
118
120
|
})),
|
|
119
121
|
// Deleting a chat stops everything in it, and nothing starts again.
|
|
120
|
-
Match.tag("ChatDeleted", (): State => ({ ...empty,
|
|
122
|
+
Match.tag("ChatDeleted", (): State => ({ ...empty, lastInputSequence: activity.lastInputSequence })),
|
|
121
123
|
Match.orElse(() => activity)
|
|
122
124
|
)
|
|
123
125
|
}
|
package/src/AssistantEngine.ts
CHANGED
|
@@ -54,7 +54,7 @@ export class AssistantEngine extends Context.Service<
|
|
|
54
54
|
chatId: ChatId,
|
|
55
55
|
input: AssistantEvent.Input | AssistantEvent.StoredAppEvent
|
|
56
56
|
) => Effect.Effect<void>
|
|
57
|
-
/** Streams the chat's stored entries after the `after`
|
|
57
|
+
/** Streams the chat's stored entries after the `after` sequence, then live ones. */
|
|
58
58
|
readonly subscribe: (assistant: string, chatId: ChatId, after: number) => Stream.Stream<AssistantEvent.Entry>
|
|
59
59
|
/**
|
|
60
60
|
* Deletes one of the assistant's chats. The chat's log records the deletion, and everything
|
|
@@ -90,7 +90,7 @@ const notRegistered = (assistant: string) =>
|
|
|
90
90
|
*
|
|
91
91
|
* const MyAssistant = Assistant.make("my-assistant")
|
|
92
92
|
*
|
|
93
|
-
* const AppLive = MyAssistant.toLayer((turn) => turn.
|
|
93
|
+
* const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: "Be brief." })).pipe(
|
|
94
94
|
* Layer.provide(ModelLive),
|
|
95
95
|
* Layer.provideMerge(AssistantEngine.layerMemory),
|
|
96
96
|
* Layer.provideMerge(ChatLog.layerMemory)
|
|
@@ -289,7 +289,7 @@ const makeClusterClient = Effect.gen(function* () {
|
|
|
289
289
|
*
|
|
290
290
|
* const MyAssistant = Assistant.make("my-assistant")
|
|
291
291
|
*
|
|
292
|
-
* const AppLive = MyAssistant.toLayer((turn) => turn.
|
|
292
|
+
* const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: "Be brief." })).pipe(
|
|
293
293
|
* Layer.provide(ModelLive),
|
|
294
294
|
* Layer.provideMerge(AssistantEngine.layerCluster),
|
|
295
295
|
* Layer.provideMerge(
|
package/src/AssistantEvent.ts
CHANGED
|
@@ -280,7 +280,7 @@ export class ModelOutput extends Schema.TaggedClass<ModelOutput>()("ModelOutput"
|
|
|
280
280
|
* arrived while the model was working. A turn has handled what its
|
|
281
281
|
* outputs had read.
|
|
282
282
|
*/
|
|
283
|
-
|
|
283
|
+
seenThroughSequence: Schema.optional(Schema.Int),
|
|
284
284
|
/** The model that wrote it, as the provider's id for it or the name the context gave it. */
|
|
285
285
|
model: Schema.optional(Schema.String),
|
|
286
286
|
/** The provider's id for the response, when it gives one. */
|
|
@@ -365,26 +365,26 @@ export class TurnStarted extends Schema.TaggedClass<TurnStarted>()("TurnStarted"
|
|
|
365
365
|
/**
|
|
366
366
|
* Represents a turn that finished.
|
|
367
367
|
*
|
|
368
|
-
* `
|
|
368
|
+
* `throughSequence` is the last entry the turn read. Inputs after it still need a turn.
|
|
369
369
|
*
|
|
370
370
|
* @category schemas
|
|
371
371
|
* @since 0.1.0
|
|
372
372
|
*/
|
|
373
373
|
export class TurnCompleted extends Schema.TaggedClass<TurnCompleted>()("TurnCompleted", {
|
|
374
|
-
|
|
374
|
+
throughSequence: Schema.Int
|
|
375
375
|
}) {}
|
|
376
376
|
|
|
377
377
|
/**
|
|
378
378
|
* Represents a turn that failed.
|
|
379
379
|
*
|
|
380
|
-
* `
|
|
380
|
+
* `throughSequence` covers everything stored when the turn failed, so a failing input isn't retried
|
|
381
381
|
* forever.
|
|
382
382
|
*
|
|
383
383
|
* @category schemas
|
|
384
384
|
* @since 0.1.0
|
|
385
385
|
*/
|
|
386
386
|
export class TurnFailed extends Schema.TaggedClass<TurnFailed>()("TurnFailed", {
|
|
387
|
-
|
|
387
|
+
throughSequence: Schema.Int,
|
|
388
388
|
message: Schema.String
|
|
389
389
|
}) {}
|
|
390
390
|
|
|
@@ -585,7 +585,7 @@ export const isAppInput = (event: { readonly _tag: string }): boolean =>
|
|
|
585
585
|
*/
|
|
586
586
|
export interface AnyEntry {
|
|
587
587
|
readonly chatId: ChatId
|
|
588
|
-
readonly
|
|
588
|
+
readonly sequence: number
|
|
589
589
|
readonly at: number
|
|
590
590
|
readonly event: { readonly _tag: string }
|
|
591
591
|
}
|
|
@@ -593,7 +593,7 @@ export interface AnyEntry {
|
|
|
593
593
|
/**
|
|
594
594
|
* Schema for a stored event with its position in the chat.
|
|
595
595
|
*
|
|
596
|
-
* `
|
|
596
|
+
* `sequence` starts at 1 and has no gaps. `at` is the time the event was stored, in epoch
|
|
597
597
|
* milliseconds.
|
|
598
598
|
*
|
|
599
599
|
* @category schemas
|
|
@@ -601,7 +601,7 @@ export interface AnyEntry {
|
|
|
601
601
|
*/
|
|
602
602
|
export const Entry = Schema.Struct({
|
|
603
603
|
chatId: ChatId,
|
|
604
|
-
|
|
604
|
+
sequence: Schema.Int,
|
|
605
605
|
at: Schema.Finite,
|
|
606
606
|
event: Schema.Union([AssistantEvent, StoredAppEvent])
|
|
607
607
|
})
|
package/src/AssistantLoop.ts
CHANGED
|
@@ -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.
|
|
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
|
*
|
|
@@ -37,7 +37,7 @@ export type Event = Loop.Event
|
|
|
37
37
|
/**
|
|
38
38
|
* Represents a chat's history as the model reads it.
|
|
39
39
|
*
|
|
40
|
-
* It holds the messages, and the last `
|
|
40
|
+
* It holds the messages, and the last `sequence` of the chat they cover.
|
|
41
41
|
*
|
|
42
42
|
* @category models
|
|
43
43
|
* @since 0.1.0
|
|
@@ -122,7 +122,7 @@ export interface StepOptions<T extends Record<string, Tool.Any> = NoTools> {
|
|
|
122
122
|
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
123
123
|
/**
|
|
124
124
|
* The current history, read at the start of the step. It is either messages, or a chat's
|
|
125
|
-
* history (`turn.history()`), whose `
|
|
125
|
+
* history (`turn.history()`), whose `throughSequence` the step's `ModelOutput` records as how far
|
|
126
126
|
* the model had read.
|
|
127
127
|
*/
|
|
128
128
|
readonly history: Effect.Effect<ReadonlyArray<Prompt.Message> | ChatHistory>
|