| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- import { produce, type WritableDraft } from "immer"
- import { SessionEvent } from "./session-event"
- import { SessionMessage } from "./session-message"
- export type MemoryState = {
- messages: SessionMessage.Message[]
- }
- export interface Adapter<Result> {
- readonly getCurrentAssistant: () => SessionMessage.Assistant | undefined
- readonly getCurrentCompaction: () => SessionMessage.Compaction | undefined
- readonly getCurrentShell: (callID: string) => SessionMessage.Shell | undefined
- readonly updateAssistant: (assistant: SessionMessage.Assistant) => void
- readonly updateCompaction: (compaction: SessionMessage.Compaction) => void
- readonly updateShell: (shell: SessionMessage.Shell) => void
- readonly appendMessage: (message: SessionMessage.Message) => void
- readonly finish: () => Result
- }
- export function memory(state: MemoryState): Adapter<MemoryState> {
- const activeAssistantIndex = () =>
- state.messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed)
- const activeCompactionIndex = () => state.messages.findLastIndex((message) => message.type === "compaction")
- const activeShellIndex = (callID: string) =>
- state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID)
- return {
- getCurrentAssistant() {
- const index = activeAssistantIndex()
- if (index < 0) return
- const assistant = state.messages[index]
- return assistant?.type === "assistant" ? assistant : undefined
- },
- getCurrentCompaction() {
- const index = activeCompactionIndex()
- if (index < 0) return
- const compaction = state.messages[index]
- return compaction?.type === "compaction" ? compaction : undefined
- },
- getCurrentShell(callID) {
- const index = activeShellIndex(callID)
- if (index < 0) return
- const shell = state.messages[index]
- return shell?.type === "shell" ? shell : undefined
- },
- updateAssistant(assistant) {
- const index = activeAssistantIndex()
- if (index < 0) return
- const current = state.messages[index]
- if (current?.type !== "assistant") return
- state.messages[index] = assistant
- },
- updateCompaction(compaction) {
- const index = activeCompactionIndex()
- if (index < 0) return
- const current = state.messages[index]
- if (current?.type !== "compaction") return
- state.messages[index] = compaction
- },
- updateShell(shell) {
- const index = activeShellIndex(shell.callID)
- if (index < 0) return
- const current = state.messages[index]
- if (current?.type !== "shell") return
- state.messages[index] = shell
- },
- appendMessage(message) {
- state.messages.push(message)
- },
- finish() {
- return state
- },
- }
- }
- export function update<Result>(adapter: Adapter<Result>, event: SessionEvent.Event): Result {
- const currentAssistant = adapter.getCurrentAssistant()
- type DraftAssistant = WritableDraft<SessionMessage.Assistant>
- type DraftTool = WritableDraft<SessionMessage.AssistantTool>
- type DraftText = WritableDraft<SessionMessage.AssistantText>
- type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning>
- const latestTool = (assistant: DraftAssistant | undefined, callID?: string) =>
- assistant?.content.findLast(
- (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID),
- )
- const latestText = (assistant: DraftAssistant | undefined) =>
- assistant?.content.findLast((item): item is DraftText => item.type === "text")
- const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) =>
- assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning" && item.id === reasoningID)
- SessionEvent.All.match(event, {
- "session.next.agent.switched": (event) => {
- adapter.appendMessage(
- new SessionMessage.AgentSwitched({
- id: event.id,
- type: "agent-switched",
- metadata: event.metadata,
- agent: event.data.agent,
- time: { created: event.data.timestamp },
- }),
- )
- },
- "session.next.model.switched": (event) => {
- adapter.appendMessage(
- new SessionMessage.ModelSwitched({
- id: event.id,
- type: "model-switched",
- metadata: event.metadata,
- model: event.data.model,
- time: { created: event.data.timestamp },
- }),
- )
- },
- "session.next.prompted": (event) => {
- adapter.appendMessage(
- new SessionMessage.User({
- id: event.id,
- type: "user",
- metadata: event.metadata,
- text: event.data.prompt.text,
- files: event.data.prompt.files,
- agents: event.data.prompt.agents,
- references: event.data.prompt.references,
- time: { created: event.data.timestamp },
- }),
- )
- },
- "session.next.synthetic": (event) => {
- adapter.appendMessage(
- new SessionMessage.Synthetic({
- sessionID: event.data.sessionID,
- text: event.data.text,
- id: event.id,
- type: "synthetic",
- time: { created: event.data.timestamp },
- }),
- )
- },
- "session.next.shell.started": (event) => {
- adapter.appendMessage(
- new SessionMessage.Shell({
- id: event.id,
- type: "shell",
- metadata: event.metadata,
- callID: event.data.callID,
- command: event.data.command,
- output: "",
- time: { created: event.data.timestamp },
- }),
- )
- },
- "session.next.shell.ended": (event) => {
- const currentShell = adapter.getCurrentShell(event.data.callID)
- if (currentShell) {
- adapter.updateShell(
- produce(currentShell, (draft) => {
- draft.output = event.data.output
- draft.time.completed = event.data.timestamp
- }),
- )
- }
- },
- "session.next.step.started": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- draft.time.completed = event.data.timestamp
- }),
- )
- }
- adapter.appendMessage(
- new SessionMessage.Assistant({
- id: event.id,
- type: "assistant",
- agent: event.data.agent,
- model: event.data.model,
- time: { created: event.data.timestamp },
- content: [],
- snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
- }),
- )
- },
- "session.next.step.ended": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- draft.time.completed = event.data.timestamp
- draft.finish = event.data.finish
- draft.cost = event.data.cost
- draft.tokens = event.data.tokens
- if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot }
- }),
- )
- }
- },
- "session.next.step.failed": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- draft.time.completed = event.data.timestamp
- draft.finish = "error"
- draft.error = event.data.error
- }),
- )
- }
- },
- "session.next.text.started": () => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- draft.content.push({
- type: "text",
- text: "",
- })
- }),
- )
- }
- },
- "session.next.text.delta": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestText(draft)
- if (match) match.text += event.data.delta
- }),
- )
- }
- },
- "session.next.text.ended": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestText(draft)
- if (match) match.text = event.data.text
- }),
- )
- }
- },
- "session.next.tool.input.started": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- draft.content.push({
- type: "tool",
- id: event.data.callID,
- name: event.data.name,
- time: {
- created: event.data.timestamp,
- },
- state: {
- status: "pending",
- input: "",
- },
- })
- }),
- )
- }
- },
- "session.next.tool.input.delta": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestTool(draft, event.data.callID)
- // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string)
- if (match && match.state.status === "pending") match.state.input += event.data.delta
- }),
- )
- }
- },
- "session.next.tool.input.ended": () => {},
- "session.next.tool.called": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestTool(draft, event.data.callID)
- if (match) {
- match.provider = event.data.provider
- match.time.ran = event.data.timestamp
- match.state = {
- status: "running",
- input: event.data.input,
- structured: {},
- content: [],
- }
- }
- }),
- )
- }
- },
- "session.next.tool.progress": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestTool(draft, event.data.callID)
- if (match && match.state.status === "running") {
- match.state.structured = event.data.structured
- match.state.content = [...event.data.content]
- }
- }),
- )
- }
- },
- "session.next.tool.success": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestTool(draft, event.data.callID)
- if (match && match.state.status === "running") {
- match.provider = event.data.provider
- match.time.completed = event.data.timestamp
- match.state = {
- status: "completed",
- input: match.state.input,
- structured: event.data.structured,
- content: [...event.data.content],
- }
- }
- }),
- )
- }
- },
- "session.next.tool.failed": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestTool(draft, event.data.callID)
- if (match && match.state.status === "running") {
- match.provider = event.data.provider
- match.time.completed = event.data.timestamp
- match.state = {
- status: "error",
- error: event.data.error,
- input: match.state.input,
- structured: match.state.structured,
- content: match.state.content,
- }
- }
- }),
- )
- }
- },
- "session.next.reasoning.started": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- draft.content.push({
- type: "reasoning",
- id: event.data.reasoningID,
- text: "",
- })
- }),
- )
- }
- },
- "session.next.reasoning.delta": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestReasoning(draft, event.data.reasoningID)
- if (match) match.text += event.data.delta
- }),
- )
- }
- },
- "session.next.reasoning.ended": (event) => {
- if (currentAssistant) {
- adapter.updateAssistant(
- produce(currentAssistant, (draft) => {
- const match = latestReasoning(draft, event.data.reasoningID)
- if (match) match.text = event.data.text
- }),
- )
- }
- },
- "session.next.retried": () => {},
- "session.next.compaction.started": (event) => {
- adapter.appendMessage(
- new SessionMessage.Compaction({
- id: event.id,
- type: "compaction",
- metadata: event.metadata,
- reason: event.data.reason,
- summary: "",
- time: { created: event.data.timestamp },
- }),
- )
- },
- "session.next.compaction.delta": (event) => {
- const currentCompaction = adapter.getCurrentCompaction()
- if (currentCompaction) {
- adapter.updateCompaction(
- produce(currentCompaction, (draft) => {
- draft.summary += event.data.text
- }),
- )
- }
- },
- "session.next.compaction.ended": (event) => {
- const currentCompaction = adapter.getCurrentCompaction()
- if (currentCompaction) {
- adapter.updateCompaction(
- produce(currentCompaction, (draft) => {
- draft.summary = event.data.text
- draft.include = event.data.include
- }),
- )
- }
- },
- })
- return adapter.finish()
- }
- export * as SessionMessageUpdater from "./session-message-updater"
|