effect-assistant 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/AppEvent.d.ts +111 -0
- package/dist/AppEvent.d.ts.map +1 -0
- package/dist/AppEvent.js +71 -0
- package/dist/AppEvent.js.map +1 -0
- package/dist/Assistant.d.ts +403 -0
- package/dist/Assistant.d.ts.map +1 -0
- package/dist/Assistant.js +126 -0
- package/dist/Assistant.js.map +1 -0
- package/dist/AssistantActivity.d.ts +85 -0
- package/dist/AssistantActivity.d.ts.map +1 -0
- package/dist/AssistantActivity.js +105 -0
- package/dist/AssistantActivity.js.map +1 -0
- package/dist/AssistantEngine.d.ts +116 -0
- package/dist/AssistantEngine.d.ts.map +1 -0
- package/dist/AssistantEngine.js +255 -0
- package/dist/AssistantEngine.js.map +1 -0
- package/dist/AssistantEvent.d.ts +614 -0
- package/dist/AssistantEvent.d.ts.map +1 -0
- package/dist/AssistantEvent.js +523 -0
- package/dist/AssistantEvent.js.map +1 -0
- package/dist/AssistantLoop.d.ts +171 -0
- package/dist/AssistantLoop.d.ts.map +1 -0
- package/dist/AssistantLoop.js +108 -0
- package/dist/AssistantLoop.js.map +1 -0
- package/dist/AssistantToolCall.d.ts +32 -0
- package/dist/AssistantToolCall.d.ts.map +1 -0
- package/dist/AssistantToolCall.js +37 -0
- package/dist/AssistantToolCall.js.map +1 -0
- package/dist/Block.d.ts +69 -0
- package/dist/Block.d.ts.map +1 -0
- package/dist/Block.js +39 -0
- package/dist/Block.js.map +1 -0
- package/dist/BlockKit.d.ts +184 -0
- package/dist/BlockKit.d.ts.map +1 -0
- package/dist/BlockKit.js +151 -0
- package/dist/BlockKit.js.map +1 -0
- package/dist/ChatId.d.ts +31 -0
- package/dist/ChatId.d.ts.map +1 -0
- package/dist/ChatId.js +36 -0
- package/dist/ChatId.js.map +1 -0
- package/dist/ChatLog.d.ts +109 -0
- package/dist/ChatLog.d.ts.map +1 -0
- package/dist/ChatLog.js +177 -0
- package/dist/ChatLog.js.map +1 -0
- package/dist/ChatView.d.ts +459 -0
- package/dist/ChatView.d.ts.map +1 -0
- package/dist/ChatView.js +496 -0
- package/dist/ChatView.js.map +1 -0
- package/dist/Chats.d.ts +110 -0
- package/dist/Chats.d.ts.map +1 -0
- package/dist/Chats.js +98 -0
- package/dist/Chats.js.map +1 -0
- package/dist/Compaction.d.ts +105 -0
- package/dist/Compaction.d.ts.map +1 -0
- package/dist/Compaction.js +133 -0
- package/dist/Compaction.js.map +1 -0
- package/dist/CurrentChat.d.ts +40 -0
- package/dist/CurrentChat.d.ts.map +1 -0
- package/dist/CurrentChat.js +33 -0
- package/dist/CurrentChat.js.map +1 -0
- package/dist/Decision.d.ts +145 -0
- package/dist/Decision.d.ts.map +1 -0
- package/dist/Decision.js +178 -0
- package/dist/Decision.js.map +1 -0
- package/dist/DecisionId.d.ts +37 -0
- package/dist/DecisionId.d.ts.map +1 -0
- package/dist/DecisionId.js +44 -0
- package/dist/DecisionId.js.map +1 -0
- package/dist/InputId.d.ts +38 -0
- package/dist/InputId.d.ts.map +1 -0
- package/dist/InputId.js +43 -0
- package/dist/InputId.js.map +1 -0
- package/dist/Instructions.d.ts +56 -0
- package/dist/Instructions.d.ts.map +1 -0
- package/dist/Instructions.js +50 -0
- package/dist/Instructions.js.map +1 -0
- package/dist/Memory.d.ts +190 -0
- package/dist/Memory.d.ts.map +1 -0
- package/dist/Memory.js +216 -0
- package/dist/Memory.js.map +1 -0
- package/dist/RunId.d.ts +41 -0
- package/dist/RunId.d.ts.map +1 -0
- package/dist/RunId.js +46 -0
- package/dist/RunId.js.map +1 -0
- package/dist/Skill.d.ts +72 -0
- package/dist/Skill.d.ts.map +1 -0
- package/dist/Skill.js +58 -0
- package/dist/Skill.js.map +1 -0
- package/dist/Specialist.d.ts +267 -0
- package/dist/Specialist.d.ts.map +1 -0
- package/dist/Specialist.js +119 -0
- package/dist/Specialist.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +108 -0
- package/dist/ToolDiscovery.d.ts.map +1 -0
- package/dist/ToolDiscovery.js +139 -0
- package/dist/ToolDiscovery.js.map +1 -0
- package/dist/ToolFailure.d.ts +66 -0
- package/dist/ToolFailure.d.ts.map +1 -0
- package/dist/ToolFailure.js +79 -0
- package/dist/ToolFailure.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +26 -0
- package/dist/internal/chat.js +202 -0
- package/dist/internal/chat.js.map +1 -0
- package/dist/internal/decisions.js +66 -0
- package/dist/internal/decisions.js.map +1 -0
- package/dist/internal/events.d.ts +46 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +65 -0
- package/dist/internal/events.js.map +1 -0
- package/dist/internal/history.js +185 -0
- package/dist/internal/history.js.map +1 -0
- package/dist/internal/instructions.js +11 -0
- package/dist/internal/instructions.js.map +1 -0
- package/dist/internal/loop.d.ts +73 -0
- package/dist/internal/loop.d.ts.map +1 -0
- package/dist/internal/loop.js +166 -0
- package/dist/internal/loop.js.map +1 -0
- package/dist/internal/schema.js +14 -0
- package/dist/internal/schema.js.map +1 -0
- package/dist/internal/skills.js +28 -0
- package/dist/internal/skills.js.map +1 -0
- package/dist/internal/specialists.js +273 -0
- package/dist/internal/specialists.js.map +1 -0
- package/dist/internal/telemetry.js +85 -0
- package/dist/internal/telemetry.js.map +1 -0
- package/dist/internal/toolkit.d.ts +76 -0
- package/dist/internal/toolkit.d.ts.map +1 -0
- package/dist/internal/toolkit.js +129 -0
- package/dist/internal/toolkit.js.map +1 -0
- package/dist/internal/tools.js +132 -0
- package/dist/internal/tools.js.map +1 -0
- package/dist/internal/turns.d.ts +64 -0
- package/dist/internal/turns.d.ts.map +1 -0
- package/dist/internal/turns.js +221 -0
- package/dist/internal/turns.js.map +1 -0
- package/dist/internal/uuid.js +45 -0
- package/dist/internal/uuid.js.map +1 -0
- package/dist/internal/writer.d.ts +12 -0
- package/dist/internal/writer.d.ts.map +1 -0
- package/package.json +51 -3
- package/src/AppEvent.ts +134 -0
- package/src/Assistant.ts +589 -0
- package/src/AssistantActivity.ts +150 -0
- package/src/AssistantEngine.ts +343 -0
- package/src/AssistantEvent.ts +635 -0
- package/src/AssistantLoop.ts +207 -0
- package/src/AssistantToolCall.ts +40 -0
- package/src/Block.ts +84 -0
- package/src/BlockKit.ts +311 -0
- package/src/ChatId.ts +36 -0
- package/src/ChatLog.ts +247 -0
- package/src/ChatView.ts +658 -0
- package/src/Chats.ts +122 -0
- package/src/Compaction.ts +224 -0
- package/src/CurrentChat.ts +47 -0
- package/src/Decision.ts +241 -0
- package/src/DecisionId.ts +46 -0
- package/src/InputId.ts +43 -0
- package/src/Instructions.ts +72 -0
- package/src/Memory.ts +276 -0
- package/src/RunId.ts +46 -0
- package/src/Skill.ts +91 -0
- package/src/Specialist.ts +355 -0
- package/src/ToolDiscovery.ts +187 -0
- package/src/ToolFailure.ts +74 -0
- package/src/index.ts +130 -0
- package/src/internal/chat.ts +372 -0
- package/src/internal/decisions.ts +131 -0
- package/src/internal/events.ts +100 -0
- package/src/internal/history.ts +366 -0
- package/src/internal/instructions.ts +9 -0
- package/src/internal/loop.ts +344 -0
- package/src/internal/schema.ts +25 -0
- package/src/internal/skills.ts +37 -0
- package/src/internal/specialists.ts +414 -0
- package/src/internal/telemetry.ts +125 -0
- package/src/internal/toolkit.ts +231 -0
- package/src/internal/tools.ts +210 -0
- package/src/internal/turns.ts +429 -0
- package/src/internal/uuid.ts +50 -0
- package/src/internal/writer.ts +12 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Option from "effect/Option"
|
|
3
|
+
import * as Schema from "effect/Schema"
|
|
4
|
+
import type * as AppEvent from "../AppEvent.ts"
|
|
5
|
+
import type * as Assistant from "../Assistant.ts"
|
|
6
|
+
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
7
|
+
import { toJsonText } from "./schema.ts"
|
|
8
|
+
|
|
9
|
+
/** @internal */
|
|
10
|
+
export interface AppEvents {
|
|
11
|
+
readonly byTag: ReadonlyMap<string, Registered>
|
|
12
|
+
readonly wakes: ReadonlySet<string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Registered {
|
|
16
|
+
readonly schema: AppEvent.EventSchema
|
|
17
|
+
readonly render: AppEvent.Render<{ readonly _tag: string }> | undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @internal */
|
|
21
|
+
export const make = (events: ReadonlyArray<AppEvent.Any>): AppEvents => {
|
|
22
|
+
const byTag = new Map(
|
|
23
|
+
events.map((event): [string, Registered] => [event.tag, { schema: event.schema, render: event.render }])
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
for (const tag of byTag.keys()) {
|
|
27
|
+
if (AssistantEvent.isLibraryEvent({ _tag: tag })) throw new Error(`${tag} is one of the library's events.`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (byTag.size !== events.length) throw new Error("An assistant's events have different tags.")
|
|
31
|
+
|
|
32
|
+
return { byTag, wakes: new Set(events.flatMap((event) => (event.wakes ? [event.tag] : []))) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @internal */
|
|
36
|
+
export const encode = Effect.fnUntraced(function* (app: AppEvents, event: { readonly _tag: string }) {
|
|
37
|
+
const registered = app.byTag.get(event._tag)
|
|
38
|
+
|
|
39
|
+
// The types only let registered events through, so one with no schema here is a defect.
|
|
40
|
+
if (registered === undefined) return yield* Effect.die(`${event._tag} isn't one of this assistant's events.`)
|
|
41
|
+
|
|
42
|
+
const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(registered.schema))(event)
|
|
43
|
+
|
|
44
|
+
return yield* Schema.decodeUnknownEffect(AssistantEvent.StoredAppEvent)(json)
|
|
45
|
+
}, Effect.orDie)
|
|
46
|
+
|
|
47
|
+
/** @internal */
|
|
48
|
+
export const isRegistered = (app: AppEvents, event: { readonly _tag: string }): boolean => app.byTag.has(event._tag)
|
|
49
|
+
|
|
50
|
+
// An app event that doesn't decode (say, one from an older version of its schema) is left out.
|
|
51
|
+
/** @internal */
|
|
52
|
+
export const decodeEntry = (app: AppEvents, entry: AssistantEvent.AnyEntry): Option.Option<Assistant.Entry<any>> => {
|
|
53
|
+
const { event } = entry
|
|
54
|
+
|
|
55
|
+
if (AssistantEvent.isLibraryEvent(event)) return Option.some({ ...toEntry(entry), event })
|
|
56
|
+
|
|
57
|
+
const registered = app.byTag.get(event._tag)
|
|
58
|
+
|
|
59
|
+
if (registered === undefined) return Option.none()
|
|
60
|
+
|
|
61
|
+
return Option.map(Schema.decodeUnknownOption(Schema.toCodecJson(registered.schema))(event), (decoded) => ({
|
|
62
|
+
...toEntry(entry),
|
|
63
|
+
event: decoded
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @internal */
|
|
68
|
+
export const decodeEntries = (
|
|
69
|
+
app: AppEvents,
|
|
70
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>
|
|
71
|
+
): ReadonlyArray<Assistant.Entry<any>> => entries.flatMap((entry) => Option.toArray(decodeEntry(app, entry)))
|
|
72
|
+
|
|
73
|
+
/** @internal */
|
|
74
|
+
export type DecodedEntries = ReturnType<typeof decodeEntries>
|
|
75
|
+
|
|
76
|
+
const decodeStored = Schema.decodeUnknownOption(AssistantEvent.StoredAppEvent)
|
|
77
|
+
|
|
78
|
+
const toEntry = (entry: AssistantEvent.AnyEntry) => ({
|
|
79
|
+
chatId: entry.chatId,
|
|
80
|
+
seq: entry.seq,
|
|
81
|
+
at: entry.at
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/** @internal */
|
|
85
|
+
export const read = (app: AppEvents, event: { readonly _tag: string }): string | undefined => {
|
|
86
|
+
const registered = app.byTag.get(event._tag)
|
|
87
|
+
|
|
88
|
+
if (registered?.render === undefined) return undefined
|
|
89
|
+
|
|
90
|
+
if (registered.render !== "json") return registered.render(event)
|
|
91
|
+
|
|
92
|
+
const stored = Schema.encodeUnknownOption(Schema.toCodecJson(registered.schema))(event).pipe(
|
|
93
|
+
Option.flatMap(decodeStored)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return Option.match(stored, {
|
|
97
|
+
onNone: () => undefined,
|
|
98
|
+
onSome: ({ _tag, ...fields }) => `<${_tag}>\n${toJsonText(fields)}\n</${_tag}>`
|
|
99
|
+
})
|
|
100
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import * as Prompt from "effect/ai/Prompt"
|
|
2
|
+
import * as Response from "effect/ai/Response"
|
|
3
|
+
import * as Match from "effect/Match"
|
|
4
|
+
import * as Option from "effect/Option"
|
|
5
|
+
import * as Predicate from "effect/Predicate"
|
|
6
|
+
import * as Schema from "effect/Schema"
|
|
7
|
+
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
8
|
+
import * as Decision from "../Decision.ts"
|
|
9
|
+
import { toJson, toJsonText } from "./schema.ts"
|
|
10
|
+
import * as Uuid from "./uuid.ts"
|
|
11
|
+
|
|
12
|
+
/** @internal */
|
|
13
|
+
export const userText = (text: string): Prompt.UserMessage =>
|
|
14
|
+
Prompt.userMessage({ content: [Prompt.textPart({ text })] })
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export const toolMessage = (result: AssistantEvent.ToolResult): Prompt.ToolMessage =>
|
|
18
|
+
Prompt.toolMessage({
|
|
19
|
+
content: [
|
|
20
|
+
Prompt.toolResultPart({
|
|
21
|
+
id: result.callId,
|
|
22
|
+
name: result.tool,
|
|
23
|
+
isFailure: result.outcome === "failure",
|
|
24
|
+
result: result.result,
|
|
25
|
+
providerExecuted: false
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// A call with no result is one a crash cut off.
|
|
31
|
+
/** @internal */
|
|
32
|
+
export const unansweredCalls = (history: ReadonlyArray<Prompt.Message>): ReadonlyArray<AssistantEvent.ToolCallPart> => {
|
|
33
|
+
const answered = new Set(
|
|
34
|
+
history.flatMap((message) =>
|
|
35
|
+
message.role === "tool" ? message.content.flatMap((part) => (part.type === "tool-result" ? [part.id] : [])) : []
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return history.flatMap((message) =>
|
|
40
|
+
message.role === "assistant"
|
|
41
|
+
? message.content.flatMap((part) =>
|
|
42
|
+
part.type === "tool-call" && !part.providerExecuted && !answered.has(part.id)
|
|
43
|
+
? [
|
|
44
|
+
Response.makePart("tool-call", {
|
|
45
|
+
id: part.id,
|
|
46
|
+
name: part.name,
|
|
47
|
+
params: toJson(part.params),
|
|
48
|
+
providerExecuted: false,
|
|
49
|
+
metadata: part.options
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
: []
|
|
53
|
+
)
|
|
54
|
+
: []
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @internal */
|
|
59
|
+
export const notRunOutput = "Not run: the turn ended before this call ran."
|
|
60
|
+
|
|
61
|
+
/** @internal */
|
|
62
|
+
export const mayHaveRunOutput =
|
|
63
|
+
"Stopped: the turn ended while this call ran. It may have done its work; check before doing it again."
|
|
64
|
+
|
|
65
|
+
const ago = (millis: number) => {
|
|
66
|
+
const seconds = Math.max(0, Math.round(millis / 1000))
|
|
67
|
+
|
|
68
|
+
return seconds < 90 ? `${seconds} seconds` : `${Math.round(seconds / 60)} minutes`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const finishedText = (
|
|
72
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
73
|
+
index: number,
|
|
74
|
+
finished: AssistantEvent.SpecialistFinished
|
|
75
|
+
) => {
|
|
76
|
+
const at = entries[index]!
|
|
77
|
+
|
|
78
|
+
const isStart = ({ event }: AssistantEvent.AnyEntry) =>
|
|
79
|
+
AssistantEvent.isLibraryEvent(event) &&
|
|
80
|
+
Predicate.isTagged(event, "SpecialistStarted") &&
|
|
81
|
+
event.runId === finished.runId
|
|
82
|
+
|
|
83
|
+
const from = entries.slice(0, index).findLastIndex(isStart)
|
|
84
|
+
const started = from === -1 ? undefined : entries[from]
|
|
85
|
+
const since = started === undefined ? [] : entries.slice(from + 1, index)
|
|
86
|
+
const wrote = since.some(({ event }) => Predicate.isTagged(event, "UserMessage"))
|
|
87
|
+
|
|
88
|
+
const lines = [
|
|
89
|
+
"A report from a specialist you started. The user hasn't seen it.",
|
|
90
|
+
started === undefined
|
|
91
|
+
? `You started it for: ${finished.goal}`
|
|
92
|
+
: `You started it ${ago(at.at - started.at)} ago for: ${finished.goal}`,
|
|
93
|
+
wrote ? "The user has written since you started it." : "The user hasn't written since you started it.",
|
|
94
|
+
Match.value(finished.status).pipe(
|
|
95
|
+
Match.when("answered", () => `Report:\n${finished.report ?? ""}`),
|
|
96
|
+
Match.when("failed", () => `It failed: ${finished.error ?? ""}`),
|
|
97
|
+
Match.when("cut_off", () => "A restart cut it off before it finished. Spawn it again if it's still needed."),
|
|
98
|
+
Match.when("stopped", () => "You stopped it."),
|
|
99
|
+
Match.exhaustive
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
return `<specialist name="${finished.specialist}" run="${finished.runId}" status="${finished.status}">\n${lines.join(
|
|
104
|
+
"\n"
|
|
105
|
+
)}\n</specialist>`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const answerText = (answer: AssistantEvent.BlockAnswer) =>
|
|
109
|
+
`<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user answered with the block on your message ${answer.messageId}.\n${toJsonText(answer.value)}\n</answer>`
|
|
110
|
+
|
|
111
|
+
/** @internal */
|
|
112
|
+
export const parkedResult = (
|
|
113
|
+
request: AssistantEvent.DecisionRequested,
|
|
114
|
+
origin: AssistantEvent.CallOrigin
|
|
115
|
+
): Prompt.ToolMessage =>
|
|
116
|
+
Prompt.toolMessage({
|
|
117
|
+
content: [
|
|
118
|
+
Prompt.toolResultPart({
|
|
119
|
+
id: origin.callId,
|
|
120
|
+
name: origin.tool,
|
|
121
|
+
isFailure: false,
|
|
122
|
+
result: {
|
|
123
|
+
status: "waiting_for_decision",
|
|
124
|
+
decisionId: request.id,
|
|
125
|
+
next: "Not run: it runs only if the user approves it. Send them a message with a decision block for this decisionId that says what they approve (the call's key facts). Their answer runs it, or declines it."
|
|
126
|
+
},
|
|
127
|
+
providerExecuted: false
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const whatDecided = (request: AssistantEvent.DecisionRequested, outcome: AssistantEvent.DecisionOutcome) =>
|
|
133
|
+
outcome === "approved"
|
|
134
|
+
? `approved: ${request.origin.tool} runs now, and its result follows.`
|
|
135
|
+
: `declined: ${request.origin.tool} didn't run, and won't.`
|
|
136
|
+
|
|
137
|
+
const decodeDecisionAnswer = Schema.decodeUnknownOption(Decision.Answer)
|
|
138
|
+
|
|
139
|
+
const decisionAnswerText = (
|
|
140
|
+
answer: AssistantEvent.BlockAnswer,
|
|
141
|
+
requests: ReadonlyMap<string, AssistantEvent.DecisionRequested>
|
|
142
|
+
) =>
|
|
143
|
+
Option.match(decodeDecisionAnswer(answer.value), {
|
|
144
|
+
onNone: () => answerText(answer),
|
|
145
|
+
onSome: ({ decisions }) => {
|
|
146
|
+
const lines = Object.entries(decisions).map(([id, outcome]) => {
|
|
147
|
+
const request = requests.get(id)
|
|
148
|
+
|
|
149
|
+
return `- ${id}: ${request === undefined ? outcome : whatDecided(request, outcome)}`
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return `<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user decided with the block on your message ${answer.messageId}:\n${lines.join("\n")}\n</answer>`
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const resultText = (result: AssistantEvent.ToolResult) =>
|
|
157
|
+
`<result tool="${result.tool}" call="${result.callId}" outcome="${result.outcome}">\nThe result of a call from earlier in this chat.\n${toJsonText(result.result)}\n</result>`
|
|
158
|
+
|
|
159
|
+
const approvedResultText = (
|
|
160
|
+
request: AssistantEvent.DecisionRequested,
|
|
161
|
+
tool: string,
|
|
162
|
+
result: AssistantEvent.ToolResult
|
|
163
|
+
) =>
|
|
164
|
+
`<decision id="${request.id}" tool="${tool}" outcome="approved">\nThe user approved it, and ${tool} ran: ${result.outcome}.\n${toJsonText(result.result)}\n</decision>`
|
|
165
|
+
|
|
166
|
+
// In the order the model saw the entries, not the order they were stored.
|
|
167
|
+
/** @internal */
|
|
168
|
+
export const fromEntries = (
|
|
169
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
170
|
+
readApp: (event: { readonly _tag: string }) => string | undefined
|
|
171
|
+
): ReadonlyArray<Prompt.Message> => {
|
|
172
|
+
const history: Array<Prompt.Message> = []
|
|
173
|
+
const held: Array<Prompt.Message> = []
|
|
174
|
+
const open = new Set<string>()
|
|
175
|
+
const requests = new Map<string, AssistantEvent.DecisionRequested>()
|
|
176
|
+
// Parked calls by call id: their result came with the parking, so a later one reads as the decision's.
|
|
177
|
+
const parked = new Map<string, AssistantEvent.DecisionRequested>()
|
|
178
|
+
const called = new Set<string>()
|
|
179
|
+
|
|
180
|
+
const flushIfClosed = () => {
|
|
181
|
+
if (open.size === 0) history.push(...held.splice(0))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const nextSeen = (from: number) => {
|
|
185
|
+
const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, "ModelOutput"))?.event
|
|
186
|
+
|
|
187
|
+
return next !== undefined && AssistantEvent.isLibraryEvent(next) && Predicate.isTagged(next, "ModelOutput")
|
|
188
|
+
? next.seenThroughSeq
|
|
189
|
+
: undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const input = (index: number, item: Prompt.Message) => {
|
|
193
|
+
const seen = nextSeen(index)
|
|
194
|
+
|
|
195
|
+
if (open.size > 0 || (seen !== undefined && seen < entries[index]!.seq)) held.push(item)
|
|
196
|
+
else history.push(item)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
entries.forEach(({ event }, index) => {
|
|
200
|
+
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
201
|
+
const text = readApp(event)
|
|
202
|
+
|
|
203
|
+
if (text !== undefined) input(index, userText(text))
|
|
204
|
+
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Match.value(event).pipe(
|
|
209
|
+
Match.tagsExhaustive({
|
|
210
|
+
UserMessage: (message) =>
|
|
211
|
+
input(index, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),
|
|
212
|
+
BlockAnswer: (answer) =>
|
|
213
|
+
input(
|
|
214
|
+
index,
|
|
215
|
+
userText(answer.kind === Decision.kind ? decisionAnswerText(answer, requests) : answerText(answer))
|
|
216
|
+
),
|
|
217
|
+
ModelOutput: (output) => {
|
|
218
|
+
history.push(...Prompt.fromResponseParts(output.parts).content)
|
|
219
|
+
|
|
220
|
+
for (const part of output.parts) {
|
|
221
|
+
if (part.type === "tool-call") {
|
|
222
|
+
open.add(part.id)
|
|
223
|
+
called.add(part.id)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
flushIfClosed()
|
|
228
|
+
},
|
|
229
|
+
ToolResult: (result) => {
|
|
230
|
+
const request = parked.get(result.callId)
|
|
231
|
+
|
|
232
|
+
if (request !== undefined) return input(index, userText(approvedResultText(request, result.tool, result)))
|
|
233
|
+
|
|
234
|
+
// A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its
|
|
235
|
+
// call right before it. An approved call's result can come long after the call was parked.
|
|
236
|
+
if (!called.has(result.callId)) return input(index, userText(resultText(result)))
|
|
237
|
+
|
|
238
|
+
history.push(toolMessage(result))
|
|
239
|
+
open.delete(result.callId)
|
|
240
|
+
flushIfClosed()
|
|
241
|
+
},
|
|
242
|
+
DecisionRequested: (request) => {
|
|
243
|
+
requests.set(request.id, request)
|
|
244
|
+
parked.set(request.origin.callId, request)
|
|
245
|
+
history.push(parkedResult(request, request.origin))
|
|
246
|
+
open.delete(request.origin.callId)
|
|
247
|
+
flushIfClosed()
|
|
248
|
+
},
|
|
249
|
+
DecisionResolved: () => undefined,
|
|
250
|
+
AssistantMessage: () => undefined,
|
|
251
|
+
ToolStarted: () => undefined,
|
|
252
|
+
TurnStarted: () => undefined,
|
|
253
|
+
TurnCompleted: () => undefined,
|
|
254
|
+
TurnFailed: () => undefined,
|
|
255
|
+
SpecialistFinished: (finished) => input(index, userText(finishedText(entries, index, finished))),
|
|
256
|
+
// Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.
|
|
257
|
+
SpecialistStarted: () => undefined,
|
|
258
|
+
BlocksClosed: () => undefined,
|
|
259
|
+
ChatDeleted: () => undefined
|
|
260
|
+
})
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return [...history, ...held]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @internal */
|
|
268
|
+
export const isInput = (event: { readonly _tag: string }, wakes: ReadonlySet<string>): boolean =>
|
|
269
|
+
AssistantEvent.isInput(event) || (!AssistantEvent.isLibraryEvent(event) && wakes.has(event._tag))
|
|
270
|
+
|
|
271
|
+
/** @internal */
|
|
272
|
+
export const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {
|
|
273
|
+
const last = entries.findLast(({ event }) => AssistantEvent.isTurnEnd(event))?.event
|
|
274
|
+
|
|
275
|
+
return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.throughSeq : 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// A run the assistant stopped isn't an input here: it wakes nothing.
|
|
279
|
+
/** @internal */
|
|
280
|
+
export const unhandledInputs = (
|
|
281
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
282
|
+
wakes: ReadonlySet<string>
|
|
283
|
+
): ReadonlyArray<{ readonly _tag: string }> => {
|
|
284
|
+
const seen = handledThrough(entries)
|
|
285
|
+
|
|
286
|
+
return entries.flatMap(({ event, seq }) =>
|
|
287
|
+
seq > seen && isInput(event, wakes) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event))
|
|
288
|
+
? [event]
|
|
289
|
+
: []
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** @internal */
|
|
294
|
+
export const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, wakes: ReadonlySet<string>): boolean =>
|
|
295
|
+
unhandledInputs(entries, wakes).length > 0
|
|
296
|
+
|
|
297
|
+
// A failure the user was shown counts as a reply.
|
|
298
|
+
/** @internal */
|
|
299
|
+
export const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSeq: number): boolean => {
|
|
300
|
+
const latest = entries.findLast(
|
|
301
|
+
({ event, seq }) => seq <= throughSeq && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
latest !== undefined &&
|
|
306
|
+
!entries.some(
|
|
307
|
+
({ event, seq }) =>
|
|
308
|
+
seq > latest.seq && (Predicate.isTagged(event, "AssistantMessage") || Predicate.isTagged(event, "TurnFailed"))
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const sentToWaitOn = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(false) }))
|
|
314
|
+
|
|
315
|
+
// Messages sent with `wait: false` are progress while the model kept working, like "On it".
|
|
316
|
+
const progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlySet<string> =>
|
|
317
|
+
new Set(
|
|
318
|
+
entries.flatMap(({ event }) =>
|
|
319
|
+
AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "ModelOutput")
|
|
320
|
+
? event.parts.flatMap((part) =>
|
|
321
|
+
part.type === "tool-call" && part.name === "send_user_message" && Option.isSome(sentToWaitOn(part.params))
|
|
322
|
+
? [Uuid.fromKey(`message:${part.id}`)]
|
|
323
|
+
: []
|
|
324
|
+
)
|
|
325
|
+
: []
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
// The model said it was on it and stopped without the result: only progress messages since the user's latest input.
|
|
330
|
+
// A specialist still working doesn't count, since its report is what comes next.
|
|
331
|
+
/** @internal */
|
|
332
|
+
export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSeq: number): boolean => {
|
|
333
|
+
const latest = entries.findLast(
|
|
334
|
+
({ event, seq }) => seq <= throughSeq && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if (latest === undefined) return false
|
|
338
|
+
|
|
339
|
+
const after = entries.flatMap(({ seq, event }) =>
|
|
340
|
+
seq > latest.seq && AssistantEvent.isLibraryEvent(event) ? [event] : []
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
const messages = after.flatMap((event) => (Predicate.isTagged(event, "AssistantMessage") ? [event] : []))
|
|
344
|
+
const progress = progressMessages(entries)
|
|
345
|
+
const started = after.flatMap((event) => (Predicate.isTagged(event, "SpecialistStarted") ? [event.runId] : []))
|
|
346
|
+
|
|
347
|
+
const finished = new Set(
|
|
348
|
+
entries.flatMap(({ event }) =>
|
|
349
|
+
AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "SpecialistFinished") ? [event.runId] : []
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
messages.length > 0 &&
|
|
355
|
+
messages.every((message) => message.block === undefined && progress.has(message.id)) &&
|
|
356
|
+
!after.some((event) => Predicate.isTagged(event, "TurnFailed")) &&
|
|
357
|
+
started.every((runId) => finished.has(runId))
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @internal */
|
|
362
|
+
export const unreadAfter = (
|
|
363
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
364
|
+
throughSeq: number,
|
|
365
|
+
wakes: ReadonlySet<string>
|
|
366
|
+
): boolean => entries.some(({ event, seq }) => seq > throughSeq && isInput(event, wakes))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Predicate from "effect/Predicate"
|
|
2
|
+
import type * as Instructions from "../Instructions.ts"
|
|
3
|
+
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const mapText = (
|
|
6
|
+
instructions: string | Instructions.Resolved,
|
|
7
|
+
f: (text: string) => string
|
|
8
|
+
): Instructions.Resolved =>
|
|
9
|
+
Predicate.isString(instructions) ? { text: f(instructions) } : { ...instructions, text: f(instructions.text) }
|