TurnEndReasonMap.error now carries a single `error: LlmFailure` field: an LlmError keeps its structured facts, any other error flattens to errorChain text under the UNKNOWN code. Consumers read message/code directly instead of defending against an unknown union — this also fixes errorChain() rendering structured failures as '[object Object]' in the TUI and ACP error paths. Document the turn-stopping contract: a concludesTurn result never short-circuits already-submitted next-step work (same-step additionalContexts or racing steering still runs), data decides.
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
/** First-party semantic text extraction for session-query consumers. */
|
|
|
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
|
/**
|
|
* Extract searchable semantic text from one first-party session event.
|
|
*
|
|
* Structural boundaries, raw stream chunks, request envelopes, and unknown
|
|
* declaration-merged events contribute no text.
|
|
* @param event - event to inspect.
|
|
* @returns newline-joined semantic text, or an empty string when non-searchable.
|
|
*/
|
|
export function extractSessionEventText(event: SessionEvent): string {
|
|
switch (event.type) {
|
|
case 'user/message':
|
|
return contentText(event.data.content)
|
|
case 'assistant/message':
|
|
return contentText(event.data.message.content)
|
|
case 'tool/call':
|
|
return joinText([event.data.name, event.data.arguments])
|
|
case 'tool/result':
|
|
return joinText([
|
|
contentText(event.data.message.content),
|
|
event.data.error?.name ?? '',
|
|
event.data.error?.code ?? '',
|
|
])
|
|
case 'todo/write':
|
|
return joinText(event.data.todos.flatMap(todo => [todo.status, todo.content]))
|
|
case 'turn/end':
|
|
return turnEndText(event.data.reason)
|
|
case 'turn/start':
|
|
case 'step/start':
|
|
case 'step/end':
|
|
case 'assistant/chunk':
|
|
case 'request/header':
|
|
return ''
|
|
// SessionEventMap is merge-extensible. Unknown events remain
|
|
// non-searchable until a concrete first-party consumer defines semantics.
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function turnEndText(reason: SessionEvent<'turn/end'>['data']['reason']): string {
|
|
switch (reason.kind) {
|
|
case 'error':
|
|
return joinText(['error', reason.error.message])
|
|
case 'aborted':
|
|
return 'aborted'
|
|
case 'max-tokens':
|
|
case 'interrupted':
|
|
return reason.kind
|
|
case 'completed':
|
|
return ''
|
|
// TurnEndReasonMap is merge-extensible. Unknown outcomes stay out until
|
|
// their owner defines which detail is semantic rather than structural.
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
type SessionContentBlock = SessionEvent<'user/message'>['data']['content'][number]
|
|
|
|
function contentText(content: readonly SessionContentBlock[]): string {
|
|
return joinText(content.flatMap(blockText))
|
|
}
|
|
|
|
function blockText(block: SessionContentBlock): string[] {
|
|
switch (block.type) {
|
|
case 'text':
|
|
return [block.text]
|
|
case 'reasoning':
|
|
return []
|
|
case 'tool-call':
|
|
return [block.name, block.arguments]
|
|
case 'tool-result':
|
|
return block.content.flatMap(blockText)
|
|
// ContentBlockMap is merge-extensible. Unknown blocks do not become
|
|
// searchable merely because their payload happens to contain strings.
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
function joinText(parts: readonly string[]): string {
|
|
return parts.map(part => part.trim()).filter(Boolean).join('\n')
|
|
}
|