@recombine-ai/bosun 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/build/client/_app/immutable/assets/0.DHvyfZ2Z.css +1 -0
  2. package/build/client/_app/immutable/assets/0.DHvyfZ2Z.css.br +0 -0
  3. package/build/client/_app/immutable/assets/0.DHvyfZ2Z.css.gz +0 -0
  4. package/build/client/_app/immutable/chunks/{CXaO9BO2.js → B6g_JkSM.js} +1 -1
  5. package/build/client/_app/immutable/chunks/B6g_JkSM.js.br +0 -0
  6. package/build/client/_app/immutable/chunks/B6g_JkSM.js.gz +0 -0
  7. package/build/client/_app/immutable/chunks/{C232NBSx.js → BcH1VEVU.js} +1 -1
  8. package/build/client/_app/immutable/chunks/BcH1VEVU.js.br +0 -0
  9. package/build/client/_app/immutable/chunks/BcH1VEVU.js.gz +0 -0
  10. package/build/client/_app/immutable/chunks/{BPR9ibgw.js → C0bQ35Gc.js} +1 -1
  11. package/build/client/_app/immutable/chunks/C0bQ35Gc.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/C0bQ35Gc.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/{C9f-9-Wt.js → CEH18jSl.js} +1 -1
  14. package/build/client/_app/immutable/chunks/CEH18jSl.js.br +0 -0
  15. package/build/client/_app/immutable/chunks/CEH18jSl.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/{CKb0QqXx.js → CSuN09OQ.js} +1 -1
  17. package/build/client/_app/immutable/chunks/CSuN09OQ.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/CSuN09OQ.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/DUMGgJTb.js +5 -0
  20. package/build/client/_app/immutable/chunks/DUMGgJTb.js.br +0 -0
  21. package/build/client/_app/immutable/chunks/DUMGgJTb.js.gz +0 -0
  22. package/build/client/_app/immutable/chunks/{BAbuLHvf.js → Deds-Bu4.js} +1 -1
  23. package/build/client/_app/immutable/chunks/Deds-Bu4.js.br +0 -0
  24. package/build/client/_app/immutable/chunks/Deds-Bu4.js.gz +0 -0
  25. package/build/client/_app/immutable/chunks/Dt0j4sZQ.js +2 -0
  26. package/build/client/_app/immutable/chunks/Dt0j4sZQ.js.br +0 -0
  27. package/build/client/_app/immutable/chunks/Dt0j4sZQ.js.gz +0 -0
  28. package/build/client/_app/immutable/chunks/{5mKQEBmA.js → DxquQJx9.js} +1 -1
  29. package/build/client/_app/immutable/chunks/DxquQJx9.js.br +0 -0
  30. package/build/client/_app/immutable/chunks/DxquQJx9.js.gz +0 -0
  31. package/build/client/_app/immutable/chunks/{COR6mdUG.js → V9U6b0dU.js} +1 -1
  32. package/build/client/_app/immutable/chunks/V9U6b0dU.js.br +0 -0
  33. package/build/client/_app/immutable/chunks/V9U6b0dU.js.gz +0 -0
  34. package/build/client/_app/immutable/entry/{app.oAXSW36_.js → app.DKvvDDfZ.js} +2 -2
  35. package/build/client/_app/immutable/entry/app.DKvvDDfZ.js.br +0 -0
  36. package/build/client/_app/immutable/entry/app.DKvvDDfZ.js.gz +0 -0
  37. package/build/client/_app/immutable/entry/start.BqlKjodc.js +1 -0
  38. package/build/client/_app/immutable/entry/start.BqlKjodc.js.br +2 -0
  39. package/build/client/_app/immutable/entry/start.BqlKjodc.js.gz +0 -0
  40. package/build/client/_app/immutable/nodes/{0.BVv0ZVNF.js → 0.CN5KvOdW.js} +1 -1
  41. package/build/client/_app/immutable/nodes/0.CN5KvOdW.js.br +0 -0
  42. package/build/client/_app/immutable/nodes/0.CN5KvOdW.js.gz +0 -0
  43. package/build/client/_app/immutable/nodes/{1.BocieXSV.js → 1.B9DiMnGP.js} +1 -1
  44. package/build/client/_app/immutable/nodes/1.B9DiMnGP.js.br +0 -0
  45. package/build/client/_app/immutable/nodes/1.B9DiMnGP.js.gz +0 -0
  46. package/build/client/_app/immutable/nodes/{15.CSwvVdKE.js → 15.DVTjsvsv.js} +1 -1
  47. package/build/client/_app/immutable/nodes/15.DVTjsvsv.js.br +0 -0
  48. package/build/client/_app/immutable/nodes/15.DVTjsvsv.js.gz +0 -0
  49. package/build/client/_app/immutable/nodes/2.Be6ngYER.js +35 -0
  50. package/build/client/_app/immutable/nodes/2.Be6ngYER.js.br +0 -0
  51. package/build/client/_app/immutable/nodes/2.Be6ngYER.js.gz +0 -0
  52. package/build/client/_app/immutable/nodes/{3.Ck1xHlrV.js → 3.CyJzwuPu.js} +1 -1
  53. package/build/client/_app/immutable/nodes/3.CyJzwuPu.js.br +0 -0
  54. package/build/client/_app/immutable/nodes/3.CyJzwuPu.js.gz +0 -0
  55. package/build/client/_app/immutable/nodes/{4.ChQxJfCA.js → 4.Ln69Bcug.js} +1 -1
  56. package/build/client/_app/immutable/nodes/4.Ln69Bcug.js.br +0 -0
  57. package/build/client/_app/immutable/nodes/4.Ln69Bcug.js.gz +0 -0
  58. package/build/client/_app/immutable/nodes/{5.zSWDxriP.js → 5.CvOWF9PV.js} +1 -1
  59. package/build/client/_app/immutable/nodes/5.CvOWF9PV.js.br +0 -0
  60. package/build/client/_app/immutable/nodes/5.CvOWF9PV.js.gz +0 -0
  61. package/build/client/_app/immutable/nodes/8.W_hIdUQV.js +2 -0
  62. package/build/client/_app/immutable/nodes/8.W_hIdUQV.js.br +0 -0
  63. package/build/client/_app/immutable/nodes/8.W_hIdUQV.js.gz +0 -0
  64. package/build/client/_app/immutable/nodes/{9.DRayViio.js → 9.DBodeuqK.js} +1 -1
  65. package/build/client/_app/immutable/nodes/9.DBodeuqK.js.br +0 -0
  66. package/build/client/_app/immutable/nodes/9.DBodeuqK.js.gz +0 -0
  67. package/build/client/_app/version.json +1 -1
  68. package/build/client/_app/version.json.br +0 -0
  69. package/build/client/_app/version.json.gz +0 -0
  70. package/build/server/chunks/{0-DWCTA9UP.js → 0-DRboob7o.js} +3 -3
  71. package/build/server/chunks/{0-DWCTA9UP.js.map → 0-DRboob7o.js.map} +1 -1
  72. package/build/server/chunks/{1-BQuoNKyB.js → 1-CN0hcf5T.js} +2 -2
  73. package/build/server/chunks/{1-BQuoNKyB.js.map → 1-CN0hcf5T.js.map} +1 -1
  74. package/build/server/chunks/{15-A9aJ00Nj.js → 15-BglDKcWD.js} +3 -3
  75. package/build/server/chunks/{15-A9aJ00Nj.js.map → 15-BglDKcWD.js.map} +1 -1
  76. package/build/server/chunks/{2-Dxx2KPQl.js → 2-BJlFzksb.js} +3 -3
  77. package/build/server/chunks/{2-Dxx2KPQl.js.map → 2-BJlFzksb.js.map} +1 -1
  78. package/build/server/chunks/{3-B1_UzEKm.js → 3-CyyHUkrh.js} +3 -3
  79. package/build/server/chunks/{3-B1_UzEKm.js.map → 3-CyyHUkrh.js.map} +1 -1
  80. package/build/server/chunks/{4-CDrszL6M.js → 4-C6AwWXAp.js} +3 -3
  81. package/build/server/chunks/{4-CDrszL6M.js.map → 4-C6AwWXAp.js.map} +1 -1
  82. package/build/server/chunks/5-BECI2rwz.js +9 -0
  83. package/build/server/chunks/{5-BZLccXTR.js.map → 5-BECI2rwz.js.map} +1 -1
  84. package/build/server/chunks/{8-DKi0Ar-v.js → 8-CFQq9WtG.js} +3 -3
  85. package/build/server/chunks/{8-DKi0Ar-v.js.map → 8-CFQq9WtG.js.map} +1 -1
  86. package/build/server/chunks/{9-Bm1zqhnj.js → 9-CC9KFXZB.js} +2 -2
  87. package/build/server/chunks/{9-Bm1zqhnj.js.map → 9-CC9KFXZB.js.map} +1 -1
  88. package/build/server/chunks/{AgentSidebar-DUAwX8yz.js → AgentSidebar-BHphbGfv.js} +2 -2
  89. package/build/server/chunks/{AgentSidebar-DUAwX8yz.js.map → AgentSidebar-BHphbGfv.js.map} +1 -1
  90. package/build/server/chunks/{ChatStepTrace-BraUG8oN.js → ChatStepTrace-Dy9ryvqZ.js} +18 -5
  91. package/build/server/chunks/ChatStepTrace-Dy9ryvqZ.js.map +1 -0
  92. package/build/server/chunks/{Icon-zMNuTF_-.js → Icon-C98HpfFT.js} +5 -1
  93. package/build/server/chunks/Icon-C98HpfFT.js.map +1 -0
  94. package/build/server/chunks/{_layout.svelte-D7OHpm3Z.js → _layout.svelte-BoUClC-X.js} +3 -3
  95. package/build/server/chunks/{_layout.svelte-D7OHpm3Z.js.map → _layout.svelte-BoUClC-X.js.map} +1 -1
  96. package/build/server/chunks/{_layout.svelte-DccBBHFo.js → _layout.svelte-DfPEqRLq.js} +22 -4
  97. package/build/server/chunks/_layout.svelte-DfPEqRLq.js.map +1 -0
  98. package/build/server/chunks/{_page.svelte-87WiaCHD.js → _page.svelte-CBh9OOQz.js} +3 -3
  99. package/build/server/chunks/{_page.svelte-87WiaCHD.js.map → _page.svelte-CBh9OOQz.js.map} +1 -1
  100. package/build/server/chunks/{_page.svelte-Ct157UWV.js → _page.svelte-DR3jiCLg.js} +3 -3
  101. package/build/server/chunks/{_page.svelte-Ct157UWV.js.map → _page.svelte-DR3jiCLg.js.map} +1 -1
  102. package/build/server/chunks/{_page.svelte-BRsvOt3E.js → _page.svelte-Dl2WmUCF.js} +3 -3
  103. package/build/server/chunks/{_page.svelte-BRsvOt3E.js.map → _page.svelte-Dl2WmUCF.js.map} +1 -1
  104. package/build/server/chunks/{_page.svelte-CaMij-Vg.js → _page.svelte-mAhccGhM.js} +384 -94
  105. package/build/server/chunks/_page.svelte-mAhccGhM.js.map +1 -0
  106. package/build/server/index.js +1 -1
  107. package/build/server/index.js.map +1 -1
  108. package/build/server/manifest.js +10 -10
  109. package/build/server/manifest.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/backend/lib/get-agent-factories.ts +45 -1
  112. package/src/backend/region/router.ts +7 -20
  113. package/src/backend/roleplay/agents.ts +40 -13
  114. package/src/backend/roleplay/router.ts +158 -3
  115. package/src/backend/roleplay/variants.spec.ts +53 -0
  116. package/src/backend/roleplay/variants.ts +325 -0
  117. package/src/backend/tests/analyze.ts +62 -1
  118. package/src/backend/tests/data.ts +97 -84
  119. package/src/backend/tests/router.ts +19 -49
  120. package/src/backend/tests/suite.ts +50 -6
  121. package/build/client/_app/immutable/assets/0.oanZa5fp.css +0 -1
  122. package/build/client/_app/immutable/assets/0.oanZa5fp.css.br +0 -0
  123. package/build/client/_app/immutable/assets/0.oanZa5fp.css.gz +0 -0
  124. package/build/client/_app/immutable/chunks/5mKQEBmA.js.br +0 -0
  125. package/build/client/_app/immutable/chunks/5mKQEBmA.js.gz +0 -0
  126. package/build/client/_app/immutable/chunks/BAbuLHvf.js.br +0 -0
  127. package/build/client/_app/immutable/chunks/BAbuLHvf.js.gz +0 -0
  128. package/build/client/_app/immutable/chunks/BH51oU1k.js +0 -2
  129. package/build/client/_app/immutable/chunks/BH51oU1k.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/BH51oU1k.js.gz +0 -0
  131. package/build/client/_app/immutable/chunks/BPR9ibgw.js.br +0 -0
  132. package/build/client/_app/immutable/chunks/BPR9ibgw.js.gz +0 -0
  133. package/build/client/_app/immutable/chunks/C232NBSx.js.br +0 -0
  134. package/build/client/_app/immutable/chunks/C232NBSx.js.gz +0 -0
  135. package/build/client/_app/immutable/chunks/C9f-9-Wt.js.br +0 -0
  136. package/build/client/_app/immutable/chunks/C9f-9-Wt.js.gz +0 -0
  137. package/build/client/_app/immutable/chunks/CKb0QqXx.js.br +0 -0
  138. package/build/client/_app/immutable/chunks/CKb0QqXx.js.gz +0 -0
  139. package/build/client/_app/immutable/chunks/COR6mdUG.js.br +0 -0
  140. package/build/client/_app/immutable/chunks/COR6mdUG.js.gz +0 -0
  141. package/build/client/_app/immutable/chunks/CXaO9BO2.js.br +0 -0
  142. package/build/client/_app/immutable/chunks/CXaO9BO2.js.gz +0 -0
  143. package/build/client/_app/immutable/chunks/jPRwMprt.js +0 -5
  144. package/build/client/_app/immutable/chunks/jPRwMprt.js.br +0 -0
  145. package/build/client/_app/immutable/chunks/jPRwMprt.js.gz +0 -0
  146. package/build/client/_app/immutable/entry/app.oAXSW36_.js.br +0 -0
  147. package/build/client/_app/immutable/entry/app.oAXSW36_.js.gz +0 -0
  148. package/build/client/_app/immutable/entry/start.C3H3zN_Q.js +0 -1
  149. package/build/client/_app/immutable/entry/start.C3H3zN_Q.js.br +0 -2
  150. package/build/client/_app/immutable/entry/start.C3H3zN_Q.js.gz +0 -0
  151. package/build/client/_app/immutable/nodes/0.BVv0ZVNF.js.br +0 -0
  152. package/build/client/_app/immutable/nodes/0.BVv0ZVNF.js.gz +0 -0
  153. package/build/client/_app/immutable/nodes/1.BocieXSV.js.br +0 -0
  154. package/build/client/_app/immutable/nodes/1.BocieXSV.js.gz +0 -0
  155. package/build/client/_app/immutable/nodes/15.CSwvVdKE.js.br +0 -0
  156. package/build/client/_app/immutable/nodes/15.CSwvVdKE.js.gz +0 -0
  157. package/build/client/_app/immutable/nodes/2.IJ8N1LYx.js +0 -35
  158. package/build/client/_app/immutable/nodes/2.IJ8N1LYx.js.br +0 -0
  159. package/build/client/_app/immutable/nodes/2.IJ8N1LYx.js.gz +0 -0
  160. package/build/client/_app/immutable/nodes/3.Ck1xHlrV.js.br +0 -0
  161. package/build/client/_app/immutable/nodes/3.Ck1xHlrV.js.gz +0 -0
  162. package/build/client/_app/immutable/nodes/4.ChQxJfCA.js.br +0 -0
  163. package/build/client/_app/immutable/nodes/4.ChQxJfCA.js.gz +0 -0
  164. package/build/client/_app/immutable/nodes/5.zSWDxriP.js.br +0 -0
  165. package/build/client/_app/immutable/nodes/5.zSWDxriP.js.gz +0 -0
  166. package/build/client/_app/immutable/nodes/8.B6qN8uAv.js +0 -2
  167. package/build/client/_app/immutable/nodes/8.B6qN8uAv.js.br +0 -0
  168. package/build/client/_app/immutable/nodes/8.B6qN8uAv.js.gz +0 -0
  169. package/build/client/_app/immutable/nodes/9.DRayViio.js.br +0 -0
  170. package/build/client/_app/immutable/nodes/9.DRayViio.js.gz +0 -0
  171. package/build/server/chunks/5-BZLccXTR.js +0 -9
  172. package/build/server/chunks/ChatStepTrace-BraUG8oN.js.map +0 -1
  173. package/build/server/chunks/Icon-zMNuTF_-.js.map +0 -1
  174. package/build/server/chunks/_layout.svelte-DccBBHFo.js.map +0 -1
  175. package/build/server/chunks/_page.svelte-CaMij-Vg.js.map +0 -1
@@ -0,0 +1,325 @@
1
+ import {
2
+ createContext,
3
+ makeAction,
4
+ type Action,
5
+ type ApiCallTrace,
6
+ type ApiCallTracer,
7
+ type ConversationalTrace,
8
+ type ConversationalTracer,
9
+ type Message,
10
+ type StepTracer,
11
+ type TestAgent,
12
+ type TestVoiceAgent,
13
+ type StepTrace
14
+ } from '@recombine-ai/engine'
15
+
16
+ import type { LogEntry } from '../../lib/ws-message'
17
+ import { getAgentFactories } from '../lib/get-agent-factories'
18
+ import { getImportNonce } from '../lib/import-nonce'
19
+ import { getRootPath } from '../lib/get-root-path'
20
+ import type { AgentState } from './agents'
21
+ import { createLogger } from './logger'
22
+ import { createScheduler } from './scheduler'
23
+ import { createStepRegistry } from './step-registry'
24
+
25
+ export type GeneratedVariant = {
26
+ text: string
27
+ state: AgentState
28
+ }
29
+
30
+ export async function generateReplyVariants(props: {
31
+ agentId: string
32
+ region?: string | null
33
+ states: AgentState[]
34
+ prevStateId: string | null
35
+ rawUserMessage: string
36
+ count: number
37
+ context: Record<string, any>
38
+ }): Promise<GeneratedVariant[]> {
39
+ const { agentId, region, states, prevStateId, rawUserMessage, count, context } = props
40
+ if (!Number.isFinite(count) || count <= 0) return []
41
+
42
+ const rootPath = await getRootPath()
43
+ const { factories } = await getAgentFactories(rootPath, getImportNonce())
44
+ const factory = factories[agentId]
45
+ if (!factory) throw new Error(`Cannot find agent ${agentId}`)
46
+
47
+ const statesById = new Map<string, AgentState>()
48
+ for (const state of states) statesById.set(state.id, clone(state))
49
+
50
+ const prevState = prevStateId ? (statesById.get(prevStateId) ?? null) : null
51
+ if (prevStateId && !prevState) {
52
+ throw new Error('Could not find the previous roleplay state for this reply')
53
+ }
54
+
55
+ const message = parseRawUserMessage(rawUserMessage)
56
+ return Promise.all(
57
+ Array.from({ length: count }, (_, index) =>
58
+ generateSingleReplyVariant({
59
+ index,
60
+ factory,
61
+ region,
62
+ context,
63
+ prevState,
64
+ statesById,
65
+ rawUserMessage,
66
+ message
67
+ })
68
+ )
69
+ )
70
+ }
71
+
72
+ export function replaceSavedState(
73
+ states: AgentState[],
74
+ targetStateId: string,
75
+ replacement: Pick<AgentState, 'history' | 'schedule'>
76
+ ): AgentState[] {
77
+ const targetIndex = states.findIndex((state) => state.id === targetStateId)
78
+ if (targetIndex === -1) throw new Error('Could not find the target state to replace')
79
+
80
+ const target = states[targetIndex]
81
+ const next = states.slice()
82
+ next[targetIndex] = {
83
+ ...target,
84
+ history: clone(replacement.history),
85
+ schedule: clone(replacement.schedule)
86
+ }
87
+ return next
88
+ }
89
+
90
+ function parseRawUserMessage(raw: string): Message {
91
+ const trimmed = String(raw ?? '').trim()
92
+ if (!trimmed) throw new Error('Could not reconstruct the user message for variants')
93
+
94
+ const imageMatch = /^\[image\]\s+(.+)$/u.exec(trimmed)
95
+ if (imageMatch) {
96
+ const imageUrl = (imageMatch[1] ?? '').trim()
97
+ if (!imageUrl) throw new Error('Image variant message is missing its URL')
98
+ return { sender: 'user', text: '', imageUrl }
99
+ }
100
+
101
+ return { sender: 'user', text: trimmed }
102
+ }
103
+
104
+ function collectMessagesUpToState(
105
+ statesById: Map<string, AgentState>,
106
+ stateId: string | null
107
+ ): Message[] {
108
+ if (!stateId) return []
109
+
110
+ const chain: AgentState[] = []
111
+ let currentId: string | null = stateId
112
+
113
+ while (currentId) {
114
+ const state = statesById.get(currentId)
115
+ if (!state) break
116
+ chain.push(state)
117
+ currentId = state.prev
118
+ }
119
+
120
+ chain.reverse()
121
+
122
+ const messages: Message[] = []
123
+ for (const state of chain) {
124
+ messages.push(...collectMessagesFromHistory(state.history))
125
+ }
126
+ return messages
127
+ }
128
+
129
+ function collectMessagesFromHistory(history: AgentState['history']): Message[] {
130
+ const messages: Message[] = []
131
+ for (const entry of history) {
132
+ if (entry.type !== 'message') continue
133
+ messages.push(entry.payload as Message)
134
+ }
135
+ return messages
136
+ }
137
+
138
+ function extractVariantText(state: AgentState): string {
139
+ for (let index = state.history.length - 1; index >= 0; index--) {
140
+ const entry = state.history[index]
141
+ if (entry.type !== 'conversational-trace') continue
142
+ const trace = entry.payload as ConversationalTrace
143
+ if (trace.eventName === 'finished-main-streaming-step' || trace.eventName === 'sent-message') {
144
+ return String(trace.content ?? '')
145
+ }
146
+ }
147
+
148
+ for (let index = state.history.length - 1; index >= 0; index--) {
149
+ const entry = state.history[index]
150
+ if (entry.type !== 'message') continue
151
+ const payload = entry.payload as Message
152
+ if (payload.sender === 'agent') return String(payload.text ?? '')
153
+ }
154
+
155
+ return ''
156
+ }
157
+
158
+ function createVariantState(prev: string | null, index: number): AgentState {
159
+ const now = new Date()
160
+ return {
161
+ id: `${now.toISOString()}::variant-${index + 1}-${Math.random().toString(36).slice(2, 8)}`,
162
+ name: `${now.toLocaleString()} (Variant ${index + 1})`,
163
+ history: [],
164
+ schedule: {},
165
+ prev,
166
+ next: null
167
+ }
168
+ }
169
+
170
+ async function generateSingleReplyVariant(props: {
171
+ index: number
172
+ factory: Awaited<ReturnType<typeof getAgentFactories>>['factories'][string]
173
+ region: string | null | undefined
174
+ context: Record<string, any>
175
+ prevState: AgentState | null
176
+ statesById: Map<string, AgentState>
177
+ rawUserMessage: string
178
+ message: Message
179
+ }): Promise<GeneratedVariant> {
180
+ const { index, factory, region, context, prevState, statesById, rawUserMessage, message } = props
181
+
182
+ const state = createVariantState(prevState?.id ?? null, index)
183
+ const scheduleSeed = clone(prevState?.schedule ?? {})
184
+
185
+ const log = (entry: LogEntry) => {
186
+ state.history.push({
187
+ type: 'log',
188
+ payload: entry
189
+ })
190
+ }
191
+ const logger = createLogger(log)
192
+
193
+ const sendAction = async (action: Action) => {
194
+ state.history.push({
195
+ type: 'action',
196
+ payload: action
197
+ })
198
+ }
199
+
200
+ const scheduler = createScheduler({
201
+ logger,
202
+ sendAction,
203
+ onUpdate: () => {}
204
+ })
205
+ const stepRegistry = createStepRegistry(logger)
206
+ const stepTracer: StepTracer = {
207
+ addStepTrace: (trace: StepTrace) => {
208
+ state.history.push({
209
+ type: 'trace',
210
+ payload: trace
211
+ })
212
+ },
213
+ async flush() {}
214
+ }
215
+ const apiCallTracer: ApiCallTracer = {
216
+ addApiCallTrace: (trace: ApiCallTrace) => {
217
+ state.history.push({
218
+ type: 'api-call-trace',
219
+ payload: trace
220
+ })
221
+ },
222
+ async flush() {}
223
+ }
224
+ const conversationalTracer: ConversationalTracer = {
225
+ addConversationalTrace: (trace: ConversationalTrace) => {
226
+ ;(trace as any).bosunStateId = state.id
227
+ ;(trace as any).bosunPrevStateId = state.prev
228
+ ;(trace as any).bosunUserMessage = rawUserMessage
229
+ state.history.push({
230
+ type: 'conversational-trace',
231
+ payload: trace
232
+ })
233
+ },
234
+ async flush() {}
235
+ }
236
+
237
+ const ctx = createContext({})
238
+ if (region) {
239
+ const current = (ctx.get() as Record<string, unknown> | null | undefined) || {}
240
+ ctx.set({ ...current, recombineRegion: region })
241
+ }
242
+ if (context && typeof context === 'object') {
243
+ ctx.swap(region ? { ...context, recombineRegion: region } : context)
244
+ }
245
+
246
+ const getMessages = () => [
247
+ ...collectMessagesUpToState(statesById, state.prev),
248
+ ...collectMessagesFromHistory(state.history)
249
+ ]
250
+
251
+ const agent = await factory({
252
+ stepTracer,
253
+ tracer: stepRegistry,
254
+ stepRegistry,
255
+ logger,
256
+ apiCallTracer,
257
+ conversationalTracer,
258
+ scheduler,
259
+ getMessages,
260
+ sendMessage: async (nextMessage: string | Message) => {
261
+ const payload =
262
+ typeof nextMessage === 'string'
263
+ ? ({ sender: 'agent', text: nextMessage } as Message)
264
+ : nextMessage
265
+
266
+ state.history.push({
267
+ type: 'message',
268
+ payload
269
+ })
270
+ },
271
+ sendAction,
272
+ ctx
273
+ })
274
+
275
+ scheduler.setState(scheduleSeed)
276
+
277
+ if (!state.prev && hasStart(agent)) {
278
+ await Promise.resolve().then(() => agent.start())
279
+ }
280
+
281
+ state.history.push({
282
+ type: 'message',
283
+ payload: message
284
+ })
285
+
286
+ if (isVoiceAgent(agent)) {
287
+ await agent.streamResponse()
288
+ } else if (await agent.isAssigned()) {
289
+ try {
290
+ const reactAct = makeAction(sendAction, 'Agent', 'React on message')
291
+ await reactAct('started')
292
+ await agent.reactOnMessage()
293
+ await reactAct('completed')
294
+
295
+ const respondAct = makeAction(sendAction, 'Agent', 'Compose reply')
296
+ await respondAct('started')
297
+ await agent.respondToMessage()
298
+ await respondAct('completed')
299
+ } catch (error) {
300
+ if (typeof (agent as any)?.onFatalError === 'function') {
301
+ await (agent as any).onFatalError(error)
302
+ } else {
303
+ throw error
304
+ }
305
+ }
306
+ }
307
+
308
+ state.schedule = clone(scheduler.serialize())
309
+ return {
310
+ text: extractVariantText(state),
311
+ state
312
+ }
313
+ }
314
+
315
+ function isVoiceAgent(agent: TestAgent): agent is TestVoiceAgent {
316
+ return 'streamResponse' in agent
317
+ }
318
+
319
+ function hasStart(agent: TestAgent): agent is TestAgent & { start: () => Promise<void> | void } {
320
+ return typeof (agent as { start?: unknown }).start === 'function'
321
+ }
322
+
323
+ function clone<T>(value: T): T {
324
+ return JSON.parse(JSON.stringify(value))
325
+ }
@@ -10,6 +10,9 @@ const DEFAULT_ANALYZE_SYSTEM_MESSAGE =
10
10
  const DEFAULT_ANALYZE_RATIONALE_SYSTEM_MESSAGE =
11
11
  'Stop roleplay. Analyze the last response. Extremely briefly and concisely, explain why you decieded to respond like that.'
12
12
 
13
+ const DEFAULT_SUGGESTED_USER_RESPONSE_PROMPT =
14
+ 'Given this conversation:\n\n{{stringifiedConversation}}\n\nPropose a realistic plausible response from the user. Be brief'
15
+
13
16
  export interface QuoteCount {
14
17
  quote: string
15
18
  count: number
@@ -30,6 +33,49 @@ export interface AnalyzeRunResult {
30
33
  raw: Record<string, unknown>
31
34
  }
32
35
 
36
+ export async function generateSuggestedUserResponse(input: {
37
+ model: string | undefined
38
+ stringifiedConversation: string
39
+ response: string
40
+ }): Promise<{ text: string }> {
41
+ const apiKey = process.env.OPENAI_API_KEY
42
+ if (!apiKey) {
43
+ throw new Error('OPENAI_API_KEY is not set')
44
+ }
45
+
46
+ const conversation = decodeHtmlEntities(String(input.stringifiedConversation ?? ''))
47
+ if (!conversation) {
48
+ throw new Error('Missing conversation')
49
+ }
50
+
51
+ const decodedResponse = decodeHtmlEntities(String(input.response ?? ''))
52
+ if (!decodedResponse.trim()) {
53
+ throw new Error('Missing response')
54
+ }
55
+
56
+ const resolvedModel = resolveModel(input.model)
57
+ if (!resolvedModel) {
58
+ throw new Error('Missing model')
59
+ }
60
+
61
+ const agentLabel = inferAgentLabel(conversation)
62
+ const fullConversation = appendResponseToConversation(conversation, decodedResponse, agentLabel)
63
+ const client = new OpenAI({ apiKey })
64
+
65
+ const completion = await client.chat.completions.create({
66
+ model: resolvedModel,
67
+ messages: [{ role: 'user', content: buildSuggestedUserResponsePrompt(fullConversation) }]
68
+ })
69
+
70
+ const content = completion.choices?.[0]?.message?.content
71
+ if (!content) throw new Error('No suggested user response from the LLM')
72
+
73
+ const text = normalizeSuggestedUserResponse(content)
74
+ if (!text) throw new Error('Suggested user response was empty')
75
+
76
+ return { text }
77
+ }
78
+
33
79
  export async function analyzeTraceQuotes(input: {
34
80
  model: string | undefined
35
81
  renderedPrompt: string
@@ -280,12 +326,27 @@ async function analyzeRationaleOnce(
280
326
  return content
281
327
  }
282
328
 
329
+ export function buildSuggestedUserResponsePrompt(stringifiedConversation: string): string {
330
+ return DEFAULT_SUGGESTED_USER_RESPONSE_PROMPT.replace(
331
+ '{{stringifiedConversation}}',
332
+ stringifiedConversation
333
+ )
334
+ }
335
+
283
336
  function normalizeQuote(raw: string): string {
284
337
  let q = decodeHtmlEntities(raw).trim()
285
338
  q = trimWrappingQuotes(q)
286
339
  return q.trim()
287
340
  }
288
341
 
342
+ export function normalizeSuggestedUserResponse(raw: string): string {
343
+ let text = decodeHtmlEntities(String(raw ?? ''))
344
+ text = stripCodeFences(text).trim()
345
+ text = text.replace(/^user:\s*/i, '')
346
+ text = trimWrappingQuotes(text)
347
+ return text.trim()
348
+ }
349
+
289
350
  function normalizeRationale(raw: string): string {
290
351
  const text = decodeHtmlEntities(String(raw ?? '')).trim()
291
352
  return stripCodeFences(text).trim()
@@ -434,7 +495,7 @@ function parseJsonArrayOfStrings(raw: string): string[] {
434
495
 
435
496
  function stripCodeFences(text: string): string {
436
497
  const trimmed = text.trim()
437
- const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)
498
+ const m = trimmed.match(/^```(?:[a-z0-9_-]+)?\s*([\s\S]*?)\s*```$/i)
438
499
  return m ? m[1] : trimmed
439
500
  }
440
501
 
@@ -6,6 +6,7 @@ import {
6
6
  readSuite,
7
7
  type TestSuite,
8
8
  upsertSuiteMeta,
9
+ withSuiteMutationLock,
9
10
  writeSuite,
10
11
  type TestRow as SuiteRow
11
12
  } from './suite'
@@ -76,39 +77,41 @@ const defaultTestRow: Omit<TestRow, 'id'> = {
76
77
 
77
78
  // Utility: ensure each row has a unique id; persist if any are missing/duplicated.
78
79
  export async function loadTests(agent: string): Promise<TestTable> {
79
- const suite = await readSuite(agent)
80
- let changed = false
81
- const seenIds = new Set<string>()
82
- const rows: TestRow[] = suite.tests.map((r) => {
83
- let id = String(r.id || '').trim()
84
- if (!id || seenIds.has(id)) {
85
- changed = true
86
- id = globalThis.crypto?.randomUUID?.() || randomUUID()
80
+ return withSuiteMutationLock(agent, async () => {
81
+ const suite = await readSuite(agent)
82
+ let changed = false
83
+ const seenIds = new Set<string>()
84
+ const rows: TestRow[] = suite.tests.map((r) => {
85
+ let id = String(r.id || '').trim()
86
+ if (!id || seenIds.has(id)) {
87
+ changed = true
88
+ id = globalThis.crypto?.randomUUID?.() || randomUUID()
89
+ }
90
+ seenIds.add(id)
91
+ // previously tests were storing context as a string...
92
+ const context = typeof r.context === 'string' ? JSON.parse(r.context) : r.context
93
+ return { ...r, context, id }
94
+ })
95
+ if (changed) {
96
+ await writeSuite(agent, { ...suite, tests: rows })
87
97
  }
88
- seenIds.add(id)
89
- // previously tests were storing context as a string...
90
- const context = typeof r.context === 'string' ? JSON.parse(r.context) : r.context
91
- return { ...r, context, id }
98
+ const headers = [
99
+ 'id',
100
+ 'pinned',
101
+ 'scenario',
102
+ 'step',
103
+ 'author',
104
+ 'category',
105
+ 'assertion_type',
106
+ 'assertion_value',
107
+ 'conversation',
108
+ 'expected_output',
109
+ 'timeout',
110
+ 'similarity_threshold',
111
+ 'context'
112
+ ]
113
+ return { rows, headers }
92
114
  })
93
- if (changed) {
94
- await writeSuite(agent, { ...suite, tests: rows })
95
- }
96
- const headers = [
97
- 'id',
98
- 'pinned',
99
- 'scenario',
100
- 'step',
101
- 'author',
102
- 'category',
103
- 'assertion_type',
104
- 'assertion_value',
105
- 'conversation',
106
- 'expected_output',
107
- 'timeout',
108
- 'similarity_threshold',
109
- 'context'
110
- ]
111
- return { rows, headers }
112
115
  }
113
116
 
114
117
  // Utility: update a single row by id and write the CSV
@@ -117,14 +120,16 @@ export async function patchTest(
117
120
  id: string,
118
121
  patch: Partial<TestRow>
119
122
  ): Promise<TestRow | null> {
120
- const suite = await readSuite(agent)
121
- const idx = suite.tests.findIndex((r) => r.id === id)
122
- if (idx < 0) return null
123
- const current = suite.tests[idx]
124
- const updated = preparePatch(current, patch)
125
- const nextRows = suite.tests.map((r, i) => (i === idx ? updated : r))
126
- await writeSuite(agent, { ...suite, tests: nextRows })
127
- return updated
123
+ return withSuiteMutationLock(agent, async () => {
124
+ const suite = await readSuite(agent)
125
+ const idx = suite.tests.findIndex((r) => r.id === id)
126
+ if (idx < 0) return null
127
+ const current = suite.tests[idx]
128
+ const updated = preparePatch(current, patch)
129
+ const nextRows = suite.tests.map((r, i) => (i === idx ? updated : r))
130
+ await writeSuite(agent, { ...suite, tests: nextRows })
131
+ return updated
132
+ })
128
133
  }
129
134
 
130
135
  function preparePatch(current: TestRow, patch: Partial<TestRow>) {
@@ -142,19 +147,21 @@ export async function patchTestsBatch(
142
147
  updates: Array<{ id: string; patch: Partial<TestRow> }>
143
148
  ): Promise<TestRow[]> {
144
149
  if (!Array.isArray(updates) || updates.length === 0) return []
145
- const suite = await readSuite(agent)
146
- const byId = new Map(suite.tests.map((r) => [r.id, r]))
147
- const updatedRows: TestRow[] = []
148
- for (const { id, patch } of updates) {
149
- const current = byId.get(id)
150
- if (!current) continue
151
- const updated = preparePatch(current, patch)
152
- byId.set(id, updated)
153
- updatedRows.push(updated)
154
- }
155
- const nextRows: TestRow[] = suite.tests.map((r) => byId.get(r.id) ?? r)
156
- await writeSuite(agent, { ...suite, tests: nextRows })
157
- return updatedRows
150
+ return withSuiteMutationLock(agent, async () => {
151
+ const suite = await readSuite(agent)
152
+ const byId = new Map(suite.tests.map((r) => [r.id, r]))
153
+ const updatedRows: TestRow[] = []
154
+ for (const { id, patch } of updates) {
155
+ const current = byId.get(id)
156
+ if (!current) continue
157
+ const updated = preparePatch(current, patch)
158
+ byId.set(id, updated)
159
+ updatedRows.push(updated)
160
+ }
161
+ const nextRows: TestRow[] = suite.tests.map((r) => byId.get(r.id) ?? r)
162
+ await writeSuite(agent, { ...suite, tests: nextRows })
163
+ return updatedRows
164
+ })
158
165
  }
159
166
 
160
167
  // Add a new row at the top
@@ -163,21 +170,23 @@ export async function addTest(
163
170
  mergeContexts: boolean,
164
171
  seed?: Partial<TestRow>
165
172
  ): Promise<TestRow> {
166
- await ensureConfigExists(agent)
167
- const suite = await loadSuiteOrEmpty(agent)
168
- const id = globalThis.crypto?.randomUUID?.() || randomUUID()
169
-
170
- const newRow: TestRow = {
171
- ...defaultTestRow,
172
- id,
173
- ...seed,
174
- context: mergeContexts
175
- ? makeContextDiff(seed?.context, suite.meta?.defaultContext)
176
- : makeContext(seed?.context)
177
- }
178
- await writeSuite(agent, { ...(suite as any), tests: [newRow, ...suite.tests] })
179
- await ensureTestExpandedByDefault(agent, newRow.id)
180
- return newRow
173
+ return withSuiteMutationLock(agent, async () => {
174
+ await ensureConfigExists(agent)
175
+ const suite = await loadSuiteOrEmpty(agent)
176
+ const id = globalThis.crypto?.randomUUID?.() || randomUUID()
177
+
178
+ const newRow: TestRow = {
179
+ ...defaultTestRow,
180
+ id,
181
+ ...seed,
182
+ context: mergeContexts
183
+ ? makeContextDiff(seed?.context, suite.meta?.defaultContext)
184
+ : makeContext(seed?.context)
185
+ }
186
+ await writeSuite(agent, { ...(suite as any), tests: [newRow, ...suite.tests] })
187
+ await ensureTestExpandedByDefault(agent, newRow.id)
188
+ return newRow
189
+ })
181
190
  }
182
191
 
183
192
  async function ensureTestExpandedByDefault(agent: string, id: string): Promise<void> {
@@ -221,27 +230,31 @@ async function loadSuiteOrEmpty(agent: string): Promise<TestSuite> {
221
230
  }
222
231
 
223
232
  export async function deleteTest(agent: string, id: string): Promise<boolean> {
224
- const suite = await readSuite(agent)
225
- const filtered = suite.tests.filter((r) => r.id !== id)
226
- if (filtered.length === suite.tests.length) return false
227
- await writeSuite(agent, { ...suite, tests: filtered })
228
- return true
233
+ return withSuiteMutationLock(agent, async () => {
234
+ const suite = await readSuite(agent)
235
+ const filtered = suite.tests.filter((r) => r.id !== id)
236
+ if (filtered.length === suite.tests.length) return false
237
+ await writeSuite(agent, { ...suite, tests: filtered })
238
+ return true
239
+ })
229
240
  }
230
241
 
231
242
  export async function reorderTests(agent: string, order: string[]): Promise<void> {
232
- const suite = await readSuite(agent)
233
- const map = new Map(suite.tests.map((r) => [r.id, r]))
234
- const next: TestRow[] = []
235
- for (const id of order) {
236
- const r = map.get(id)
237
- if (r) {
238
- next.push(r)
239
- map.delete(id)
243
+ await withSuiteMutationLock(agent, async () => {
244
+ const suite = await readSuite(agent)
245
+ const map = new Map(suite.tests.map((r) => [r.id, r]))
246
+ const next: TestRow[] = []
247
+ for (const id of order) {
248
+ const r = map.get(id)
249
+ if (r) {
250
+ next.push(r)
251
+ map.delete(id)
252
+ }
240
253
  }
241
- }
242
- // Append any rows that were not included in order (safety)
243
- for (const r of map.values()) next.push(r)
244
- await writeSuite(agent, { ...suite, tests: next })
254
+ // Append any rows that were not included in order (safety)
255
+ for (const r of map.values()) next.push(r)
256
+ await writeSuite(agent, { ...suite, tests: next })
257
+ })
245
258
  }
246
259
 
247
260
  export async function loadConfig(agent: string): Promise<RunConfig> {