mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-24 13:25:01 +00:00
418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
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"
|