|
|
@@ -1,12 +1,8 @@
|
|
|
import { castDraft, produce, type WritableDraft } from "immer"
|
|
|
-import { DateTime, Effect } from "effect"
|
|
|
+import { DateTime, Effect, Match, pipe } from "effect"
|
|
|
import { SessionEvent } from "./event"
|
|
|
import { SessionMessage } from "./message"
|
|
|
|
|
|
-export type MemoryState = {
|
|
|
- messages: SessionMessage.Info[]
|
|
|
-}
|
|
|
-
|
|
|
export interface Adapter {
|
|
|
readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never>
|
|
|
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
|
|
|
@@ -23,89 +19,7 @@ export interface Adapter {
|
|
|
readonly appendMessage: (message: SessionMessage.Info) => Effect.Effect<void, never, never>
|
|
|
}
|
|
|
|
|
|
-export function memory(state: MemoryState): Adapter {
|
|
|
- const assistantIndex = (messageID: SessionMessage.ID) =>
|
|
|
- state.messages.findLastIndex((message) => message.id === messageID)
|
|
|
- const shellIndex = (messageID: SessionMessage.ID) =>
|
|
|
- state.messages.findLastIndex((message) => message.id === messageID)
|
|
|
- const compactionIndex = () =>
|
|
|
- state.messages.findLastIndex((message) => message.type === "compaction" && message.status === "running")
|
|
|
- // A newer step supersedes stale incomplete rows; never resume an older assistant projection.
|
|
|
- const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
|
|
|
-
|
|
|
- return {
|
|
|
- getModel() {
|
|
|
- return Effect.sync(
|
|
|
- () =>
|
|
|
- state.messages.findLast(
|
|
|
- (message): message is SessionMessage.ModelSelected | SessionMessage.Assistant =>
|
|
|
- message.type === "model-switched" || message.type === "assistant",
|
|
|
- )?.model,
|
|
|
- )
|
|
|
- },
|
|
|
- getCurrentAssistant() {
|
|
|
- return Effect.sync(() => {
|
|
|
- const index = latestAssistantIndex()
|
|
|
- if (index < 0) return
|
|
|
- const assistant = state.messages[index]
|
|
|
- return assistant?.type === "assistant" && !assistant.time.completed ? assistant : undefined
|
|
|
- })
|
|
|
- },
|
|
|
- getAssistant(messageID) {
|
|
|
- return Effect.sync(() => {
|
|
|
- const index = assistantIndex(messageID)
|
|
|
- if (index < 0) return
|
|
|
- const assistant = state.messages[index]
|
|
|
- return assistant?.type === "assistant" ? assistant : undefined
|
|
|
- })
|
|
|
- },
|
|
|
- getShell(shellID) {
|
|
|
- return Effect.sync(() => {
|
|
|
- return state.messages.find((message): message is SessionMessage.Shell => {
|
|
|
- return message.type === "shell" && message.shellID === shellID
|
|
|
- })
|
|
|
- })
|
|
|
- },
|
|
|
- getCompaction() {
|
|
|
- return Effect.sync(() => {
|
|
|
- const index = compactionIndex()
|
|
|
- const message = state.messages[index]
|
|
|
- return message?.type === "compaction" ? message : undefined
|
|
|
- })
|
|
|
- },
|
|
|
- updateAssistant(assistant) {
|
|
|
- return Effect.sync(() => {
|
|
|
- const index = assistantIndex(assistant.id)
|
|
|
- if (index < 0) return
|
|
|
- const current = state.messages[index]
|
|
|
- if (current?.type !== "assistant") return
|
|
|
- state.messages[index] = assistant
|
|
|
- })
|
|
|
- },
|
|
|
- updateShell(shell) {
|
|
|
- return Effect.sync(() => {
|
|
|
- const index = shellIndex(shell.id)
|
|
|
- if (index < 0) return
|
|
|
- const current = state.messages[index]
|
|
|
- if (current?.type !== "shell") return
|
|
|
- state.messages[index] = shell
|
|
|
- })
|
|
|
- },
|
|
|
- updateCompaction(compaction) {
|
|
|
- return Effect.sync(() => {
|
|
|
- const index = state.messages.findLastIndex((message) => message.id === compaction.id)
|
|
|
- if (index >= 0) state.messages[index] = compaction
|
|
|
- })
|
|
|
- },
|
|
|
- appendMessage(message) {
|
|
|
- return Effect.sync(() => {
|
|
|
- state.messages.push(message)
|
|
|
- })
|
|
|
- },
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
+export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
|
|
type DraftAssistant = WritableDraft<SessionMessage.Assistant>
|
|
|
type DraftTool = WritableDraft<SessionMessage.AssistantTool>
|
|
|
type DraftText = WritableDraft<SessionMessage.AssistantText>
|
|
|
@@ -139,9 +53,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- return Effect.gen(function* () {
|
|
|
- yield* SessionEvent.All.match(event, {
|
|
|
- "session.usage.updated": () => Effect.void,
|
|
|
+ const project = pipe(
|
|
|
+ Match.type<SessionEvent.DurableEvent>(),
|
|
|
+ Match.discriminatorsExhaustive("type")({
|
|
|
"session.usage.recorded": () => Effect.void,
|
|
|
"session.agent.selected": (event) => {
|
|
|
return adapter.appendMessage(
|
|
|
@@ -321,12 +235,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
draft.content.push(castDraft(SessionMessage.AssistantText.make({ type: "text", text: "" })))
|
|
|
})
|
|
|
},
|
|
|
- "session.text.delta": (event) => {
|
|
|
- return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
|
|
- const match = latestText(draft)
|
|
|
- if (match) match.text += event.data.delta
|
|
|
- })
|
|
|
- },
|
|
|
"session.text.ended": (event) => {
|
|
|
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
|
|
const match = latestText(draft)
|
|
|
@@ -351,7 +259,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
)
|
|
|
})
|
|
|
},
|
|
|
- "session.tool.input.delta": () => Effect.void,
|
|
|
"session.tool.input.ended": (event) => {
|
|
|
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
|
|
const match = latestTool(draft, event.data.id)
|
|
|
@@ -375,14 +282,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
}
|
|
|
})
|
|
|
},
|
|
|
- "session.tool.progress": (event) => {
|
|
|
- return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
|
|
- const match = latestTool(draft, event.data.id)
|
|
|
- if (match && match.state.status === "running") {
|
|
|
- match.state.metadata = event.data.metadata
|
|
|
- }
|
|
|
- })
|
|
|
- },
|
|
|
// Terminal tool events are self-contained; projection is a direct copy and
|
|
|
// never reaches into ephemeral progress history.
|
|
|
"session.tool.success": (event) => {
|
|
|
@@ -436,12 +335,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
)
|
|
|
})
|
|
|
},
|
|
|
- "session.reasoning.delta": (event) => {
|
|
|
- return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
|
|
- const match = latestReasoning(draft)
|
|
|
- if (match) match.text += event.data.delta
|
|
|
- })
|
|
|
- },
|
|
|
"session.reasoning.ended": (event) => {
|
|
|
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
|
|
const match = latestReasoning(draft)
|
|
|
@@ -475,12 +368,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
time: { created: event.created },
|
|
|
}),
|
|
|
),
|
|
|
- "session.compaction.delta": (event) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const current = yield* adapter.getCompaction()
|
|
|
- if (current?.status !== "running") return
|
|
|
- yield* adapter.updateCompaction({ ...current, summary: current.summary + event.data.text })
|
|
|
- }),
|
|
|
"session.compaction.ended": (event) => {
|
|
|
return Effect.gen(function* () {
|
|
|
const current = yield* adapter.getCompaction()
|
|
|
@@ -526,8 +413,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|
|
"session.revert.staged": () => Effect.void,
|
|
|
"session.revert.cleared": () => Effect.void,
|
|
|
"session.revert.committed": () => Effect.void,
|
|
|
- })
|
|
|
- })
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ return project(event)
|
|
|
}
|
|
|
|
|
|
export * as SessionMessageUpdater from "./message-updater"
|