forked from tiann/hapi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvoiceContextPlan.ts
More file actions
136 lines (119 loc) · 4.16 KB
/
Copy pathvoiceContextPlan.ts
File metadata and controls
136 lines (119 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import {
ELEVENLABS_WEBRTC_CONTEXT_MAX_BYTES,
VOICE_CONTEXT_STREAM_CHUNK_MAX_BYTES,
truncateUtf8ByteLength,
utf8ByteLength
} from '@hapi/protocol/voice-personality'
import type { DecryptedMessage, Session } from '@/types/api'
import { formatMessage } from './contextFormatters'
import { VOICE_CONFIG } from '../voiceConfig'
const BOOTSTRAP_RECENT_MESSAGES = 2
export interface SessionVoiceContextPlan {
bootstrap: string
streamChunks: string[]
truncated: boolean
totalMessages: number
messagesInBootstrap: number
messagesStreamed: number
notice: string | null
}
function formatSessionHeader(session: Session): string {
const summary = session.metadata?.summary?.text?.trim()
const path = session.metadata?.path
const lines = [
'THIS IS AN ACTIVE SESSION.',
`# Session ID: ${session.id}`,
path ? `# Project path: ${path}` : '',
summary ? `# Session summary:\n${summary}` : ''
].filter(Boolean)
return lines.join('\n\n')
}
function sortedMessages(messages: DecryptedMessage[]): DecryptedMessage[] {
return [...messages].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0))
}
function chunkTextByBytes(parts: string[], maxBytes: number): string[] {
const chunks: string[] = []
let current = ''
const flush = () => {
if (current.trim()) {
chunks.push(current.trim())
current = ''
}
}
for (const part of parts) {
const candidate = current ? `${current}\n\n${part}` : part
if (utf8ByteLength(candidate) <= maxBytes) {
current = candidate
continue
}
flush()
if (utf8ByteLength(part) <= maxBytes) {
current = part
} else {
chunks.push(truncateUtf8ByteLength(part, maxBytes))
}
}
flush()
return chunks
}
/**
* Small handshake context for startSession; remainder is streamed after connect.
*
* `agentLabel` is the display label for the session's agent flavor
* (e.g. "Claude", "Cursor", "Codex"); voiceHooks computes it once per call.
*/
export function buildSessionVoiceContextPlan(
session: Session | null,
messages: DecryptedMessage[],
agentLabel: string
): SessionVoiceContextPlan {
if (!session) {
return {
bootstrap: 'Session not available',
streamChunks: [],
truncated: false,
totalMessages: 0,
messagesInBootstrap: 0,
messagesStreamed: 0,
notice: null
}
}
const all = sortedMessages(messages)
const capped = VOICE_CONFIG.MAX_HISTORY_MESSAGES > 0
? all.slice(-VOICE_CONFIG.MAX_HISTORY_MESSAGES)
: all
const formatted = capped
.map((m) => formatMessage(m, agentLabel))
.filter((line): line is string => Boolean(line))
const recentCount = Math.min(BOOTSTRAP_RECENT_MESSAGES, formatted.length)
const bootstrapMessages = formatted.slice(-recentCount)
const streamMessages = formatted.slice(0, -recentCount)
let bootstrap = formatSessionHeader(session)
if (bootstrapMessages.length > 0) {
bootstrap += '\n\n## Recent messages\n\n' + bootstrapMessages.join('\n\n')
}
const bootstrapBeforeCap = bootstrap
bootstrap = truncateUtf8ByteLength(bootstrap, ELEVENLABS_WEBRTC_CONTEXT_MAX_BYTES)
const streamChunks = chunkTextByBytes(
streamMessages.map((line) => `[Session history]\n${line}`),
VOICE_CONTEXT_STREAM_CHUNK_MAX_BYTES
)
const truncated = bootstrap.length < bootstrapBeforeCap.length
|| streamMessages.length < formatted.length - recentCount
let notice: string | null = null
if (streamChunks.length > 0) {
notice = `Streaming ${streamChunks.length} context update(s) after connect (${streamMessages.length} older messages).`
}
if (truncated) {
notice = [notice, 'Some session context was shortened to fit voice wire limits.'].filter(Boolean).join(' ')
}
return {
bootstrap,
streamChunks,
truncated,
totalMessages: formatted.length,
messagesInBootstrap: bootstrapMessages.length,
messagesStreamed: streamMessages.length,
notice
}
}