effect-assistant 0.1.0 → 0.3.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/README.md +1 -1
- package/dist/Assistant.d.ts +74 -28
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +11 -4
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +10 -16
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +11 -4
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +39 -1
- package/dist/AssistantEngine.d.ts.map +1 -1
- package/dist/AssistantEngine.js +81 -28
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +52 -70
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +28 -57
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +7 -5
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +6 -3
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +14 -18
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +4 -7
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.js +1 -1
- package/dist/Compaction.d.ts +8 -4
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +12 -15
- package/dist/Compaction.js.map +1 -1
- package/dist/CurrentChat.d.ts +2 -1
- package/dist/CurrentChat.d.ts.map +1 -1
- package/dist/CurrentChat.js.map +1 -1
- package/dist/Instructions.js +1 -1
- package/dist/Skill.d.ts +33 -0
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +27 -1
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +4 -1
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +482 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +572 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +4 -5
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolFailure.d.ts +1 -1
- package/dist/ToolFailure.js +1 -1
- package/dist/ToolFailure.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +4 -7
- package/dist/internal/chat.js +11 -37
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.d.ts +15 -16
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +29 -28
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +26 -58
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/input.js +7 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/loop.d.ts +2 -2
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +21 -56
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/skills.js +6 -1
- package/dist/internal/skills.js.map +1 -1
- package/dist/internal/specialists.js +7 -7
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +6 -20
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +10 -31
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +2 -21
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -43
- package/dist/internal/turns.js.map +1 -1
- package/package.json +3 -2
- package/src/Assistant.ts +75 -29
- package/src/AssistantActivity.ts +11 -18
- package/src/AssistantEngine.ts +110 -43
- package/src/AssistantEvent.ts +46 -74
- package/src/AssistantLoop.ts +7 -5
- package/src/Block.ts +1 -1
- package/src/BlockKit.ts +18 -31
- package/src/Compaction.ts +8 -21
- package/src/CurrentChat.ts +2 -1
- package/src/Skill.ts +34 -1
- package/src/Specialist.ts +4 -1
- package/src/Timeline.ts +861 -0
- package/src/ToolDiscovery.ts +5 -13
- package/src/ToolFailure.ts +1 -1
- package/src/index.ts +5 -20
- package/src/internal/chat.ts +8 -57
- package/src/internal/events.ts +43 -43
- package/src/internal/history.ts +36 -101
- package/src/internal/input.ts +3 -0
- package/src/internal/loop.ts +31 -101
- package/src/internal/skills.ts +11 -0
- package/src/internal/specialists.ts +6 -9
- package/src/internal/toolkit.ts +17 -60
- package/src/internal/tools.ts +2 -53
- package/src/internal/turns.ts +37 -79
- package/dist/AppEvent.d.ts +0 -111
- package/dist/AppEvent.d.ts.map +0 -1
- package/dist/AppEvent.js +0 -71
- package/dist/AppEvent.js.map +0 -1
- package/dist/ChatView.d.ts +0 -459
- package/dist/ChatView.d.ts.map +0 -1
- package/dist/ChatView.js +0 -496
- package/dist/ChatView.js.map +0 -1
- package/dist/Decision.d.ts +0 -145
- package/dist/Decision.d.ts.map +0 -1
- package/dist/Decision.js +0 -178
- package/dist/Decision.js.map +0 -1
- package/dist/DecisionId.d.ts +0 -37
- package/dist/DecisionId.d.ts.map +0 -1
- package/dist/DecisionId.js +0 -44
- package/dist/DecisionId.js.map +0 -1
- package/dist/internal/decisions.js +0 -66
- package/dist/internal/decisions.js.map +0 -1
- package/src/AppEvent.ts +0 -134
- package/src/ChatView.ts +0 -658
- package/src/Decision.ts +0 -241
- package/src/DecisionId.ts +0 -46
- package/src/internal/decisions.ts +0 -131
package/dist/internal/turns.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { ToolResult, TurnCompleted, TurnFailed, TurnStarted, isLibraryEvent } from "../AssistantEvent.js";
|
|
2
|
-
import {
|
|
3
|
-
import { approvedCalls } from "./decisions.js";
|
|
2
|
+
import { decodeEntries } from "./events.js";
|
|
4
3
|
import { toJsonText } from "./schema.js";
|
|
5
|
-
import { decodeEntries, read } from "./events.js";
|
|
6
4
|
import { fromEntries, mayHaveRunOutput, notRunOutput, owesDelivery, owesReply, unansweredCalls, unhandledInputs, unreadAfter, userText } from "./history.js";
|
|
7
5
|
import { agentInput, agentOutput, agentSpan, currentTraceId } from "./telemetry.js";
|
|
8
6
|
import { duplicateNames, fromToolkitOrEmpty, merge, provide, runOnly } from "./toolkit.js";
|
|
@@ -10,16 +8,17 @@ import { makeMessaging, sendUserMessageName, waitToolName } from "./tools.js";
|
|
|
10
8
|
import { TurnTools, run as run$1 } from "./loop.js";
|
|
11
9
|
import { withCatalog } from "../Skill.js";
|
|
12
10
|
import { protocol } from "../Instructions.js";
|
|
13
|
-
import { toTools } from "./skills.js";
|
|
11
|
+
import { available, toTools } from "./skills.js";
|
|
14
12
|
import { mapText } from "./instructions.js";
|
|
15
13
|
import { toolkit } from "./specialists.js";
|
|
16
|
-
import * as Predicate from "effect/Predicate";
|
|
17
14
|
import "effect/Context";
|
|
18
15
|
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Predicate from "effect/Predicate";
|
|
19
17
|
import * as Schema from "effect/Schema";
|
|
20
18
|
import * as Stream from "effect/Stream";
|
|
21
19
|
import * as Exit from "effect/Exit";
|
|
22
20
|
import * as Option from "effect/Option";
|
|
21
|
+
import * as Ref from "effect/Ref";
|
|
23
22
|
import * as Cause from "effect/Cause";
|
|
24
23
|
import { identity } from "effect/Function";
|
|
25
24
|
//#region src/internal/turns.ts
|
|
@@ -32,8 +31,6 @@ const deliveryNudge = "You told the user you're on it, but stopped without givin
|
|
|
32
31
|
/** @internal */
|
|
33
32
|
const register = Effect.fnUntraced(function* (definition, options) {
|
|
34
33
|
const model = yield* Effect.context();
|
|
35
|
-
const decisionBlock = definition.blocks?.byKind.get(kind);
|
|
36
|
-
if (decisionBlock !== void 0 && decisionBlock !== block) return yield* Effect.die(`The ${kind} block kind is the library's: use Decision.block.`);
|
|
37
34
|
const specialists = yield* Effect.forEach(definition.specialists, Effect.fnUntraced(function* (specialist) {
|
|
38
35
|
const built = yield* Effect.serviceOption(specialist.Handler);
|
|
39
36
|
if (Option.isNone(built)) return yield* Effect.die(`No handler for the ${specialist.name} specialist: provide ${specialist.name}.toLayer.`);
|
|
@@ -60,28 +57,10 @@ const failureMessage = (cause) => Option.match(Schema.decodeUnknownOption(Provid
|
|
|
60
57
|
onSome: ({ _tag, raw }) => `${_tag ?? "Error"}: ${Predicate.isString(raw) ? raw : toJsonText(raw)}`
|
|
61
58
|
});
|
|
62
59
|
const lastSeq = (entries, otherwise) => entries.at(-1)?.seq ?? otherwise;
|
|
63
|
-
const toHistory = (viewed, app) => {
|
|
64
|
-
const out = [];
|
|
65
|
-
let run = [];
|
|
66
|
-
const flush = () => {
|
|
67
|
-
out.push(...fromEntries(run, (event) => read(app, event)));
|
|
68
|
-
run = [];
|
|
69
|
-
};
|
|
70
|
-
for (const item of viewed) if (isEntry(item)) run.push(item);
|
|
71
|
-
else {
|
|
72
|
-
flush();
|
|
73
|
-
out.push(item);
|
|
74
|
-
}
|
|
75
|
-
flush();
|
|
76
|
-
return out;
|
|
77
|
-
};
|
|
78
|
-
const isEntry = (item) => Predicate.hasProperty(item, "seq");
|
|
79
60
|
const closeOpenCalls = Effect.fnUntraced(function* (chat, startSeq) {
|
|
80
61
|
const entries = (yield* chat.entries).filter(({ seq }) => seq > startSeq);
|
|
81
62
|
const started = new Set(entries.flatMap(({ event }) => Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []));
|
|
82
|
-
const
|
|
83
|
-
const approved = approvedCalls(all).filter((call) => started.has(call.id));
|
|
84
|
-
const open = [...unansweredCalls(fromEntries(entries, () => void 0)), ...approved];
|
|
63
|
+
const open = unansweredCalls(fromEntries(entries));
|
|
85
64
|
if (open.length === 0) return;
|
|
86
65
|
yield* chat.append(open.map((call) => ToolResult.make({
|
|
87
66
|
callId: call.id,
|
|
@@ -97,9 +76,9 @@ const closeOpenCalls = Effect.fnUntraced(function* (chat, startSeq) {
|
|
|
97
76
|
})));
|
|
98
77
|
});
|
|
99
78
|
const handledThrough = (entries, startSeq) => entries.reduce((through, { seq, event }) => seq > startSeq && isLibraryEvent(event) && Predicate.isTagged(event, "ModelOutput") && event.seenThroughSeq !== void 0 ? Math.max(through, event.seenThroughSeq) : through, startSeq);
|
|
100
|
-
const finishTurn = Effect.fnUntraced(function* (chat, read,
|
|
79
|
+
const finishTurn = Effect.fnUntraced(function* (chat, read, ranLoop) {
|
|
101
80
|
const entries = yield* chat.entries;
|
|
102
|
-
const end = owesReply(entries, read) && !unreadAfter(entries, read
|
|
81
|
+
const end = ranLoop && owesReply(entries, read) && !unreadAfter(entries, read) ? TurnFailed.make({
|
|
103
82
|
throughSeq: read,
|
|
104
83
|
message: silentTurnMessage
|
|
105
84
|
}) : TurnCompleted.make({ throughSeq: read });
|
|
@@ -113,14 +92,15 @@ const failTurn = Effect.fnUntraced(function* (chat, cause) {
|
|
|
113
92
|
message: failureMessage(cause)
|
|
114
93
|
})]);
|
|
115
94
|
});
|
|
116
|
-
const makeTurnApi = (registration, chat, startSeq, checkOpen) => {
|
|
95
|
+
const makeTurnApi = (registration, chat, startSeq, checkOpen, ranLoop) => {
|
|
117
96
|
const { definition, builtins } = registration;
|
|
118
97
|
const { app } = definition;
|
|
98
|
+
const running = (stream) => Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)));
|
|
119
99
|
const history = Effect.fnUntraced(function* (view = identity) {
|
|
120
100
|
yield* checkOpen;
|
|
121
101
|
const all = yield* chat.entries;
|
|
122
102
|
return {
|
|
123
|
-
messages:
|
|
103
|
+
messages: fromEntries(view(decodeEntries(app, all))),
|
|
124
104
|
throughSeq: lastSeq(all, startSeq)
|
|
125
105
|
};
|
|
126
106
|
});
|
|
@@ -128,7 +108,7 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen) => {
|
|
|
128
108
|
const replyPass = (options) => Stream.unwrap(Effect.gen(function* () {
|
|
129
109
|
const all = yield* chat.entries;
|
|
130
110
|
const through = handledThrough(all, startSeq);
|
|
131
|
-
if (unreadAfter(all, through
|
|
111
|
+
if (unreadAfter(all, through)) return Stream.empty;
|
|
132
112
|
const owed = owesReply(all, through);
|
|
133
113
|
const undelivered = !owed && owesDelivery(all, through);
|
|
134
114
|
if (!owed && !undelivered) return Stream.empty;
|
|
@@ -140,24 +120,25 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen) => {
|
|
|
140
120
|
yield* checkOpen;
|
|
141
121
|
const entries = yield* chat.entries;
|
|
142
122
|
return {
|
|
143
|
-
messages: [...
|
|
123
|
+
messages: [...fromEntries(options.view(decodeEntries(app, entries))), userText(nudge)],
|
|
144
124
|
throughSeq: lastSeq(entries, startSeq)
|
|
145
125
|
};
|
|
146
126
|
}),
|
|
147
127
|
maxSteps: 2
|
|
148
128
|
});
|
|
149
129
|
}));
|
|
150
|
-
const replyIfOwed = (options) => replyPass({
|
|
130
|
+
const replyIfOwed = (options) => running(replyPass({
|
|
151
131
|
instructions: instructions(options.instructions),
|
|
152
132
|
toolkit: builtins,
|
|
153
133
|
view: identity,
|
|
154
134
|
nudge: options.nudge
|
|
155
|
-
});
|
|
135
|
+
}));
|
|
156
136
|
const prepare = (options) => {
|
|
157
|
-
const
|
|
137
|
+
const own = fromToolkitOrEmpty(options.toolkit);
|
|
138
|
+
const skills = available(options.skills ?? [], [...builtins.definitions, ...own.definitions]);
|
|
158
139
|
return {
|
|
159
140
|
system: mapText(instructions(options.instructions), (text) => withCatalog(text, skills)),
|
|
160
|
-
toolkit: merge(merge(builtins, toTools(skills)),
|
|
141
|
+
toolkit: merge(merge(builtins, toTools(skills)), own),
|
|
161
142
|
view: options.history ?? identity
|
|
162
143
|
};
|
|
163
144
|
};
|
|
@@ -169,19 +150,18 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen) => {
|
|
|
169
150
|
loop: (options) => {
|
|
170
151
|
const { system, toolkit, view } = prepare(options);
|
|
171
152
|
const twice = duplicateNames(toolkit);
|
|
172
|
-
const
|
|
173
|
-
const loop = twice.length > 0 ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(", ")}`) : deciding.length > 0 && definition.blocks?.byKind.get("decision") === void 0 ? Stream.die(`${deciding.map((tool) => tool.name).join(", ")} can wait for the user's decision: add Decision.block to the assistant's blocks.`) : run$1({
|
|
153
|
+
const loop = twice.length > 0 ? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(", ")}`) : run$1({
|
|
174
154
|
instructions: system,
|
|
175
155
|
toolkit,
|
|
176
156
|
history: history(view),
|
|
177
157
|
maxSteps: options.maxSteps ?? 16
|
|
178
158
|
});
|
|
179
|
-
return Stream.concat(loop, replyPass({
|
|
159
|
+
return running(Stream.concat(loop, replyPass({
|
|
180
160
|
instructions: system,
|
|
181
161
|
toolkit,
|
|
182
162
|
view,
|
|
183
163
|
nudge: options.replyNudge
|
|
184
|
-
}));
|
|
164
|
+
})));
|
|
185
165
|
}
|
|
186
166
|
};
|
|
187
167
|
};
|
|
@@ -189,7 +169,7 @@ const makeTurnApi = (registration, chat, startSeq, checkOpen) => {
|
|
|
189
169
|
const run = Effect.fnUntraced(function* (options) {
|
|
190
170
|
const { chat } = options;
|
|
191
171
|
const before = yield* chat.entries;
|
|
192
|
-
yield* agentInput(unhandledInputs(before
|
|
172
|
+
yield* agentInput(unhandledInputs(before));
|
|
193
173
|
const assistant = options.registration.definition.name;
|
|
194
174
|
const traceId = yield* currentTraceId;
|
|
195
175
|
const started = yield* chat.append([TurnStarted.make(Option.match(traceId, {
|
|
@@ -200,12 +180,13 @@ const run = Effect.fnUntraced(function* (options) {
|
|
|
200
180
|
})
|
|
201
181
|
}))]);
|
|
202
182
|
const startSeq = lastSeq(started, 0);
|
|
203
|
-
const
|
|
183
|
+
const ranLoop = yield* Ref.make(false);
|
|
184
|
+
const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop);
|
|
204
185
|
const record = (event) => chat.append([event]);
|
|
205
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({
|
|
206
187
|
onSuccess: Effect.fnUntraced(function* () {
|
|
207
188
|
const all = yield* chat.entries;
|
|
208
|
-
yield* finishTurn(chat, handledThrough(all, startSeq),
|
|
189
|
+
yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop));
|
|
209
190
|
}),
|
|
210
191
|
onFailure: (cause) => failTurn(chat, cause)
|
|
211
192
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turns.js","names":["Decision.kind","Decision.block","Toolkits.merge","Tools.makeMessaging","Specialists.toolkit","History.fromEntries","AppEvents.read","Decisions.approvedCalls","History.unansweredCalls","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","Skill.withCatalog","Skills.toTools","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 type * as Tool from \"effect/ai/Tool\"\nimport type * as Prompt from \"effect/ai/Prompt\"\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 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 Decision from \"../Decision.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 Decisions from \"./decisions.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 decisionBlock = definition.blocks?.byKind.get(Decision.kind)\n\n if (decisionBlock !== undefined && decisionBlock !== Decision.block) {\n return yield* Effect.die(`The ${Decision.kind} block kind is the library's: use Decision.block.`)\n }\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\nconst toHistory = (\n viewed: ReadonlyArray<Assistant.Entry<any> | Prompt.Message>,\n app: AppEvents.AppEvents\n): ReadonlyArray<Prompt.Message> => {\n const out: Array<Prompt.Message> = []\n let run: Array<AssistantEvent.AnyEntry> = []\n\n const flush = () => {\n out.push(...History.fromEntries(run, (event) => AppEvents.read(app, event)))\n run = []\n }\n\n for (const item of viewed) {\n if (isEntry(item)) run.push(item)\n else {\n flush()\n out.push(item)\n }\n }\n\n flush()\n\n return out\n}\n\nconst isEntry = (item: Assistant.Entry<any> | Prompt.Message): item is Assistant.Entry<any> =>\n Predicate.hasProperty(item, \"seq\")\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 // Approved calls this turn started that have no result are cut off like any other; one not started yet waits.\n const all = yield* chat.entries\n const approved = Decisions.approvedCalls(all).filter((call) => started.has(call.id))\n const open = [...History.unansweredCalls(History.fromEntries(entries, () => undefined)), ...approved]\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// Fails visibly when the user's latest input got no reply; an input that arrived after what its model read gets the\n// next turn, which replies.\nconst finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, wakes: ReadonlySet<string>) {\n const entries = yield* chat.entries\n\n const end =\n History.owesReply(entries, read) && !History.unreadAfter(entries, read, wakes)\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): Assistant.Turn<any> => {\n const { definition, builtins } = registration\n const { app } = definition\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: toHistory(view(AppEvents.decodeEntries(app, all)), app),\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, app.wakes)) 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 ...toHistory(options.view(AppEvents.decodeEntries(app, entries)), app),\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 replyPass({\n instructions: instructions(options.instructions),\n toolkit: builtins,\n view: identity,\n nudge: options.nudge\n })\n\n const prepare = <T extends Record<string, Tool.Any>, RS>(options: Assistant.LoopOptions<T, RS, any>) => {\n const skills = options.skills ?? []\n\n return {\n system: mapText(instructions(options.instructions), (text) => Skill.withCatalog(text, skills)),\n toolkit: Toolkits.merge(\n Toolkits.merge(builtins, Skills.toTools(skills)),\n Toolkits.fromToolkitOrEmpty(options.toolkit)\n ),\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 deciding = toolkit.definitions.filter(\n (tool) => tool.needsApproval !== undefined && tool.needsApproval !== false\n )\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 : deciding.length > 0 && definition.blocks?.byKind.get(Decision.kind) === undefined\n ? Stream.die(\n `${deciding.map((tool) => tool.name).join(\", \")} can wait for the user's decision: add Decision.block to the assistant's blocks.`\n )\n : Loop.run({\n instructions: system,\n toolkit,\n history: history(view),\n maxSteps: options.maxSteps ?? 16\n })\n\n return 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, options.registration.definition.app.wakes))\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 turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen)\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), options.registration.definition.app.wakes)\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","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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A colleague assistant on Effect: an append-only event log per chat, an interruptible agent loop, specialists, skills, and UI blocks",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,5 +50,6 @@
|
|
|
50
50
|
"build": "vp pack",
|
|
51
51
|
"test": "vp test run --exclude test/contract.test.ts",
|
|
52
52
|
"test:contract": "vp test run test/contract.test.ts"
|
|
53
|
-
}
|
|
53
|
+
},
|
|
54
|
+
"types": "./dist/index.d.ts"
|
|
54
55
|
}
|
package/src/Assistant.ts
CHANGED
|
@@ -24,7 +24,6 @@ import * as Predicate from "effect/Predicate"
|
|
|
24
24
|
import * as Schema from "effect/Schema"
|
|
25
25
|
import type * as Scope from "effect/Scope"
|
|
26
26
|
import * as Stream from "effect/Stream"
|
|
27
|
-
import type * as AppEvent from "./AppEvent.ts"
|
|
28
27
|
import { AssistantEngine } from "./AssistantEngine.ts"
|
|
29
28
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
30
29
|
import type * as AssistantLoop from "./AssistantLoop.ts"
|
|
@@ -66,7 +65,8 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
66
65
|
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
67
66
|
/**
|
|
68
67
|
* Playbooks the model can follow. The prompt lists them as a catalog, and the model loads one
|
|
69
|
-
* with `load_skill` when a task matches it.
|
|
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
70
|
*/
|
|
71
71
|
readonly skills?: ReadonlyArray<Skill<RS>> | undefined
|
|
72
72
|
/** The maximum number of model calls per turn. Defaults to 16. */
|
|
@@ -75,7 +75,8 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
75
75
|
readonly replyNudge?: string | undefined
|
|
76
76
|
/**
|
|
77
77
|
* Selects which of the chat's entries the model reads, for example only those since the last
|
|
78
|
-
* summary. Defaults to all
|
|
78
|
+
* summary, and maps the app's own events it should read into prompt messages. Defaults to all
|
|
79
|
+
* entries, without the app's events.
|
|
79
80
|
*/
|
|
80
81
|
readonly history?: HistoryView<Events> | undefined
|
|
81
82
|
}
|
|
@@ -86,9 +87,43 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
86
87
|
*
|
|
87
88
|
* **Details**
|
|
88
89
|
*
|
|
89
|
-
* The function receives the chat's entries
|
|
90
|
-
* `Prompt.Message`s. The model reads the returned entries the same way
|
|
91
|
-
* and reads each prompt message unchanged.
|
|
90
|
+
* The function receives the chat's entries, the app's own events among them, and returns, in
|
|
91
|
+
* order, a mix of entries and `Prompt.Message`s. The model reads the returned entries the same way
|
|
92
|
+
* it reads the full history, and reads each prompt message unchanged, where the view put it. The
|
|
93
|
+
* model reads none of the app's events by itself: map those it should read to a message, and an
|
|
94
|
+
* app event's entry left in the result is dropped.
|
|
95
|
+
*
|
|
96
|
+
* A message stands right after the entry before it: one that came after the model's last read, or
|
|
97
|
+
* between a tool call and its result, moves after them, as an input does.
|
|
98
|
+
*
|
|
99
|
+
* **Example** (Telling the model about an order the user placed)
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { Assistant, InputId } from "effect-assistant"
|
|
103
|
+
* import * as Prompt from "effect/ai/Prompt"
|
|
104
|
+
* import * as Schema from "effect/Schema"
|
|
105
|
+
*
|
|
106
|
+
* class OrderPlaced extends Schema.TaggedClass<OrderPlaced>()("OrderPlaced", {
|
|
107
|
+
* id: InputId.InputId,
|
|
108
|
+
* reference: Schema.String
|
|
109
|
+
* }) {}
|
|
110
|
+
*
|
|
111
|
+
* const MyAssistant = Assistant.make("my-assistant", { events: [OrderPlaced] })
|
|
112
|
+
*
|
|
113
|
+
* const withOrders: Assistant.HistoryView<OrderPlaced> = (entries) =>
|
|
114
|
+
* entries.map((entry) =>
|
|
115
|
+
* entry.event instanceof OrderPlaced
|
|
116
|
+
* ? Prompt.userMessage({
|
|
117
|
+
* content: [Prompt.textPart({ text: `The user placed order ${entry.event.reference}.` })]
|
|
118
|
+
* })
|
|
119
|
+
* : entry
|
|
120
|
+
* )
|
|
121
|
+
*
|
|
122
|
+
* // An `OrderPlaced` that the API submits wakes a turn, which reads it as this note.
|
|
123
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
124
|
+
* turn.loop({ instructions: "Be brief.", history: withOrders })
|
|
125
|
+
* )
|
|
126
|
+
* ```
|
|
92
127
|
*
|
|
93
128
|
* @category models
|
|
94
129
|
* @since 0.1.0
|
|
@@ -249,8 +284,8 @@ export interface Chat<Events = never> {
|
|
|
249
284
|
readonly entries: Effect.Effect<ReadonlyArray<Entry<Events>>>
|
|
250
285
|
/**
|
|
251
286
|
* Appends events after the chat's last entry and sends them to the chat's subscribers. The
|
|
252
|
-
* events can be the library's or the assistant's own, such as a summary.
|
|
253
|
-
* turn
|
|
287
|
+
* events can be the library's or the assistant's own, such as a summary. None of them wakes a
|
|
288
|
+
* turn: only what comes in through the assistant's `submit` does.
|
|
254
289
|
*/
|
|
255
290
|
append(...events: ReadonlyArray<AssistantEvent.AssistantEvent | Events>): Effect.Effect<void>
|
|
256
291
|
}
|
|
@@ -266,8 +301,11 @@ export interface Chat<Events = never> {
|
|
|
266
301
|
export interface Service<Events = never> {
|
|
267
302
|
/**
|
|
268
303
|
* Stores an input or one of the assistant's own events, then returns. Examples are a user's
|
|
269
|
-
* message or block answer, a workflow's progress, or the
|
|
270
|
-
*
|
|
304
|
+
* message or block answer, a workflow's progress, or the order the user placed. Each one is an
|
|
305
|
+
* input: storing it wakes a turn, which the assistant runs in the background. The handler decides
|
|
306
|
+
* what the turn does with it, and an app event the model should read needs a history view that
|
|
307
|
+
* maps it (see {@link HistoryView}). To store an app event without waking a turn, append it from
|
|
308
|
+
* within the chat instead (`yield* MyAssistant.Chat`).
|
|
271
309
|
*
|
|
272
310
|
* Fails with `GetChatError` when the app's `Chats` service reports that the chat can't be used,
|
|
273
311
|
* because the app doesn't know the chat or has deleted it.
|
|
@@ -279,9 +317,10 @@ export interface Service<Events = never> {
|
|
|
279
317
|
* A reader that already has the earlier entries passes `after` and receives only the entries
|
|
280
318
|
* after that `seq`.
|
|
281
319
|
*
|
|
282
|
-
* For a chat client, pipe the whole log into `
|
|
283
|
-
* streams the chat as its user sees it. Its `after` goes there, not here:
|
|
284
|
-
* depends on earlier entries.
|
|
320
|
+
* For a chat client, pipe the whole log into `Timeline.changes(MyAssistant, { after })`, which
|
|
321
|
+
* streams the chat's timeline, as its user sees it. Its `after` goes there, not here: a message
|
|
322
|
+
* on the timeline depends on earlier entries. The timeline shows only the app events named in its
|
|
323
|
+
* `show`, none by default (see `Timeline.changes`).
|
|
285
324
|
*/
|
|
286
325
|
readonly subscribe: (
|
|
287
326
|
chatId: ChatId,
|
|
@@ -323,8 +362,8 @@ export interface Assistant<
|
|
|
323
362
|
readonly blocks: BlockKit.BlockKit | undefined
|
|
324
363
|
/** The specialists the assistant can hand work to. */
|
|
325
364
|
readonly specialists: ReadonlyArray<Specs>
|
|
326
|
-
/** The app's own events the assistant
|
|
327
|
-
readonly events: ReadonlyArray<
|
|
365
|
+
/** The schemas of the app's own events the assistant stores. */
|
|
366
|
+
readonly events: ReadonlyArray<AssistantEvent.AppEventSchema>
|
|
328
367
|
/**
|
|
329
368
|
* Creates a layer that registers the assistant's handler with the engine and provides the
|
|
330
369
|
* assistant's service.
|
|
@@ -387,9 +426,9 @@ export interface Assistant<
|
|
|
387
426
|
*
|
|
388
427
|
* export const MyAssistant = Assistant.make("my-assistant")
|
|
389
428
|
*
|
|
390
|
-
* // Requires `
|
|
429
|
+
* // Requires `Sharding`. The cluster's runners run the chats, with `AssistantEngine.layerCluster`.
|
|
391
430
|
* const MyAssistantClient = MyAssistant.layerClient.pipe(
|
|
392
|
-
* Layer.provide(AssistantEngine.
|
|
431
|
+
* Layer.provide(AssistantEngine.layerClusterClient)
|
|
393
432
|
* )
|
|
394
433
|
* ```
|
|
395
434
|
*/
|
|
@@ -419,28 +458,35 @@ export type Any = Assistant<any, any, any>
|
|
|
419
458
|
*/
|
|
420
459
|
export const isAssistant = (u: unknown): u is Any => Predicate.hasProperty(u, TypeId)
|
|
421
460
|
|
|
422
|
-
function entrySchema<E>(events: ReadonlyArray<
|
|
461
|
+
function entrySchema<E>(events: ReadonlyArray<AssistantEvent.AppEventSchema>): Schema.Codec<Entry<E>, Schema.Json>
|
|
423
462
|
// The overload states the decoded type: the library's events, or one of these schemas' types.
|
|
424
|
-
function entrySchema(events: ReadonlyArray<
|
|
463
|
+
function entrySchema(events: ReadonlyArray<AssistantEvent.AppEventSchema>): Schema.Top {
|
|
425
464
|
return Schema.toCodecJson(
|
|
426
465
|
Schema.Struct({
|
|
427
466
|
chatId: ChatId,
|
|
428
467
|
seq: Schema.Int,
|
|
429
468
|
at: Schema.Finite,
|
|
430
|
-
event: Schema.Union([AssistantEvent.AssistantEvent, ...events
|
|
469
|
+
event: Schema.Union([AssistantEvent.AssistantEvent, ...events])
|
|
431
470
|
})
|
|
432
471
|
)
|
|
433
472
|
}
|
|
434
473
|
|
|
435
474
|
/**
|
|
436
|
-
* Creates an assistant definition from its name, its UI blocks, its specialists, and the
|
|
437
|
-
* events it stores
|
|
475
|
+
* Creates an assistant definition from its name, its UI blocks, its specialists, and the schemas of
|
|
476
|
+
* the app's own events it stores.
|
|
438
477
|
*
|
|
439
478
|
* **Details**
|
|
440
479
|
*
|
|
441
480
|
* The definition holds no code that runs turns. Provide a handler with its `toLayer`, then yield
|
|
442
481
|
* the definition to get the assistant's {@link Service}.
|
|
443
482
|
*
|
|
483
|
+
* The definition only says which of the app's events a chat stores. What happens with one is up
|
|
484
|
+
* to the places that use it. One that comes in through the service's `submit`, from outside a
|
|
485
|
+
* turn, is an input and wakes a turn, like a user's message; one a turn appends to its chat never
|
|
486
|
+
* does. The handler decides what a turn does, a history view says how the model reads an event
|
|
487
|
+
* ({@link HistoryView}), and the chat's timeline says which its user sees
|
|
488
|
+
* (`Timeline.changes`).
|
|
489
|
+
*
|
|
444
490
|
* **Example** (Defining an assistant and submitting a message)
|
|
445
491
|
*
|
|
446
492
|
* ```ts
|
|
@@ -466,7 +512,7 @@ function entrySchema(events: ReadonlyArray<AppEvent.Any>): Schema.Top {
|
|
|
466
512
|
export const make = <
|
|
467
513
|
const Name extends string,
|
|
468
514
|
const Specs extends ReadonlyArray<Specialist.Any> = readonly [],
|
|
469
|
-
const Events extends ReadonlyArray<
|
|
515
|
+
const Events extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []
|
|
470
516
|
>(
|
|
471
517
|
name: Name,
|
|
472
518
|
options?: {
|
|
@@ -478,14 +524,14 @@ export const make = <
|
|
|
478
524
|
*/
|
|
479
525
|
readonly specialists?: Specs | undefined
|
|
480
526
|
/**
|
|
481
|
-
* The app's own events, each
|
|
482
|
-
* `
|
|
483
|
-
* decoded.
|
|
484
|
-
*
|
|
527
|
+
* The schemas of the app's own events, each a tagged struct or class. `submit` and the chat's
|
|
528
|
+
* `append` store them, in their JSON form, and the chat's `entries` and `subscribe` read them
|
|
529
|
+
* back, decoded. Each needs a tag of its own that isn't one of the library's: `make` throws
|
|
530
|
+
* otherwise.
|
|
485
531
|
*/
|
|
486
532
|
readonly events?: Events | undefined
|
|
487
533
|
}
|
|
488
|
-
): Assistant<Name, Specs[number],
|
|
534
|
+
): Assistant<Name, Specs[number], Events[number]["Type"]> => {
|
|
489
535
|
const events = options?.events ?? []
|
|
490
536
|
|
|
491
537
|
const definition: Turns.Definition = {
|
|
@@ -495,7 +541,7 @@ export const make = <
|
|
|
495
541
|
app: AppEvents.make(events)
|
|
496
542
|
}
|
|
497
543
|
|
|
498
|
-
type OwnEvents =
|
|
544
|
+
type OwnEvents = Events[number]["Type"]
|
|
499
545
|
|
|
500
546
|
const Key = Context.Service<Identifier<Name>, Service<OwnEvents>>(`effect-assistant/Assistant/${name}`)
|
|
501
547
|
|
package/src/AssistantActivity.ts
CHANGED
|
@@ -71,32 +71,25 @@ const withoutRun = (activity: State, runId: RunId): State => {
|
|
|
71
71
|
return { ...activity, specialists }
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* Represents the options for folding a chat's entries into its activity.
|
|
76
|
-
*
|
|
77
|
-
* @category models
|
|
78
|
-
* @since 0.1.0
|
|
79
|
-
*/
|
|
80
|
-
export interface Options {
|
|
81
|
-
/**
|
|
82
|
-
* The tags of the app's events that wake a turn (see `AppEvent`). These events count as inputs
|
|
83
|
-
* too. Defaults to none.
|
|
84
|
-
*/
|
|
85
|
-
readonly wakes?: ReadonlySet<string> | undefined
|
|
86
|
-
}
|
|
87
|
-
|
|
88
74
|
/**
|
|
89
75
|
* Returns the activity after one more entry. Fold a chat's entries in order, or use
|
|
90
76
|
* {@link fromEntries}.
|
|
91
77
|
*
|
|
78
|
+
* **Details**
|
|
79
|
+
*
|
|
80
|
+
* One of the app's events counts as an input, and a turn as about to start, when it came in
|
|
81
|
+
* through the assistant's `submit`. The log marks it so (`AssistantEvent.isAppInput`), as
|
|
82
|
+
* `ChatLog.read` returns it; `subscribe` streams it decoded, without the mark, so there the turn
|
|
83
|
+
* shows from its `TurnStarted`.
|
|
84
|
+
*
|
|
92
85
|
* @category combinators
|
|
93
86
|
* @since 0.1.0
|
|
94
87
|
*/
|
|
95
|
-
export const apply = (activity: State, entry: AssistantEvent.AnyEntry
|
|
88
|
+
export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State => {
|
|
96
89
|
const event = entry.event
|
|
97
90
|
|
|
98
91
|
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
99
|
-
return
|
|
92
|
+
return AssistantEvent.isAppInput(event) ? { ...activity, queued: true, lastInputSeq: entry.seq } : activity
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
if (AssistantEvent.isInput(event)) {
|
|
@@ -136,8 +129,8 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry, options?:
|
|
|
136
129
|
* @category constructors
|
|
137
130
|
* @since 0.1.0
|
|
138
131
|
*/
|
|
139
|
-
export const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry
|
|
140
|
-
entries.reduce((activity, entry) => apply(activity, entry
|
|
132
|
+
export const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): State =>
|
|
133
|
+
entries.reduce((activity, entry) => apply(activity, entry), empty)
|
|
141
134
|
|
|
142
135
|
/**
|
|
143
136
|
* Returns whether anything is working: a turn that is running or about to start, or a specialist
|