feat(core): finalize session event lifecycle (#35272)

This commit is contained in:
Kit Langton 2026-07-06 18:14:06 -04:00 committed by GitHub
commit bf01264661
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 5200 additions and 3711 deletions

View file

@ -54,6 +54,7 @@ type Data = {
// session ID in that family, including the key itself once its info arrives.
family: Record<string, string[]>
status: Record<string, DataSessionStatus>
compaction: Partial<Record<string, string>>
message: Record<string, SessionMessage[]>
input: Record<string, string[]>
permission: Record<string, PermissionV2Request[]>
@ -90,6 +91,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
info: {},
family: {},
status: {},
compaction: {},
message: {},
input: {},
permission: {},
@ -149,14 +151,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
item.type === "tool" && (callID === undefined || item.id === callID),
)
},
latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
return assistant?.content.findLast(
(item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
)
latestText(assistant: SessionMessageAssistant | undefined) {
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text")
},
latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
latestReasoning(assistant: SessionMessageAssistant | undefined) {
return assistant?.content.findLast(
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID,
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && !item.time?.completed,
)
},
}
@ -273,7 +273,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
setStore("session", "info", event.data.sessionID, "title", event.data.title)
break
case "session.prompt.promoted": {
setSessionStatus(event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
const position = index.get(event.data.inputID)
if (position === undefined) return
@ -335,7 +334,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.shell.started":
setSessionStatus(event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: messageIDFromEvent(event.id),
@ -346,7 +344,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.shell.ended":
setSessionStatus(event.data.sessionID, "idle")
message.update(event.data.sessionID, (draft) => {
const match = message.shell(draft, event.data.shell.id)
if (!match) return
@ -356,11 +353,24 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.step.started":
setSessionStatus(event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
if (index.has(event.data.assistantMessageID)) return
const position = index.get(event.data.assistantMessageID)
const existing = position === undefined ? undefined : draft[position]
if (existing?.type === "assistant") {
existing.agent = event.data.agent
existing.model = event.data.model
existing.retry = undefined
existing.error = undefined
existing.finish = undefined
existing.time.completed = undefined
if (event.data.snapshot) existing.snapshot = { ...existing.snapshot, start: event.data.snapshot }
return
}
const currentAssistant = message.activeAssistant(draft)
if (currentAssistant) currentAssistant.time.completed = event.created
if (currentAssistant) {
currentAssistant.retry = undefined
currentAssistant.time.completed = event.created
}
message.append(draft, index, {
id: event.data.assistantMessageID,
type: "assistant",
@ -373,7 +383,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.step.ended":
setSessionStatus(event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
if (!currentAssistant) return
@ -392,32 +401,26 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
currentAssistant.time.completed = event.created
currentAssistant.finish = "error"
currentAssistant.error = event.data.error
currentAssistant.retry = undefined
})
break
case "session.text.started":
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "text",
id: event.data.textID,
text: "",
})
})
break
case "session.text.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(
message.assistant(draft, index, event.data.assistantMessageID),
event.data.textID,
)
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text += event.data.delta
})
break
case "session.text.ended":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(
message.assistant(draft, index, event.data.assistantMessageID),
event.data.textID,
)
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text = event.data.text
})
break
@ -458,7 +461,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
)
if (!match) return
match.time.ran = event.created
match.provider = event.data.provider
match.executed = event.data.executed
match.providerState = event.data.state
match.state = { status: "running", input: event.data.input, structured: {}, content: [] }
})
break
@ -487,11 +491,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
content: [...event.data.content],
result: event.data.result,
}
match.provider = {
executed: event.data.provider.executed || match.provider?.executed === true,
metadata: match.provider?.metadata,
resultMetadata: event.data.provider.metadata,
}
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.time.completed = event.created
})
break
@ -510,11 +511,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
content: match.state.status === "running" ? match.state.content : [],
result: event.data.result,
}
match.provider = {
executed: event.data.provider.executed || match.provider?.executed === true,
metadata: match.provider?.metadata,
resultMetadata: event.data.provider.metadata,
}
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.time.completed = event.created
})
break
@ -522,41 +520,55 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "reasoning",
id: event.data.reasoningID,
text: "",
providerMetadata: event.data.providerMetadata,
state: event.data.state,
time: { created: event.created },
})
})
break
case "session.reasoning.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(
message.assistant(draft, index, event.data.assistantMessageID),
event.data.reasoningID,
)
const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text += event.data.delta
})
break
case "session.reasoning.ended":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(
message.assistant(draft, index, event.data.assistantMessageID),
event.data.reasoningID,
)
const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID))
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? event.created, completed: event.created }
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
if (event.data.state !== undefined) match.state = event.data.state
}
})
break
case "session.retried":
case "session.compaction.started":
case "session.retry.scheduled":
message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
if (!currentAssistant) return
currentAssistant.retry = {
attempt: event.data.attempt,
at: event.data.at,
error: event.data.error,
}
})
break
case "session.execution.started":
setSessionStatus(event.data.sessionID, "running")
break
case "session.execution.settled":
case "session.compaction.started":
setStore("session", "compaction", event.data.sessionID, "")
break
case "session.execution.succeeded":
case "session.execution.failed":
case "session.execution.interrupted":
setSessionStatus(event.data.sessionID, "idle")
if (store.session.compaction[event.data.sessionID] !== undefined)
setStore("session", "compaction", event.data.sessionID, undefined)
message.update(event.data.sessionID, (draft) => {
const currentAssistant = message.activeAssistant(draft)
if (currentAssistant) currentAssistant.retry = undefined
})
break
case "session.revert.staged":
if (store.session.info[event.data.sessionID])
@ -573,17 +585,19 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
"session",
"input",
event.data.sessionID,
(store.session.input[event.data.sessionID] ?? []).filter((id) => id < event.data.messageID),
(store.session.input[event.data.sessionID] ?? []).filter((id) => id < event.data.to),
)
message.update(event.data.sessionID, (draft, index) => {
const position = draft.findIndex((item) => item.id >= event.data.messageID)
const position = draft.findIndex((item) => item.id >= event.data.to)
if (position === -1) return
for (const item of draft.splice(position)) index.delete(item.id)
})
break
case "session.compaction.delta":
setStore("session", "compaction", event.data.sessionID, (text) => (text ?? "") + event.data.text)
break
case "session.compaction.ended":
setStore("session", "compaction", event.data.sessionID, undefined)
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: messageIDFromEvent(event.id),
@ -710,6 +724,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return store.session.input[sessionID]?.includes(inputID) ?? false
},
},
compaction(sessionID: string) {
return store.session.compaction[sessionID]
},
async refresh(sessionID: string) {
setStore("session", "info", sessionID, mutable(await sdk.api.session.get({ sessionID })))
registerSession(sessionID)