@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.
- package/build/client/_app/immutable/assets/0.DHvyfZ2Z.css +1 -0
- package/build/client/_app/immutable/assets/0.DHvyfZ2Z.css.br +0 -0
- package/build/client/_app/immutable/assets/0.DHvyfZ2Z.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{CXaO9BO2.js → B6g_JkSM.js} +1 -1
- package/build/client/_app/immutable/chunks/B6g_JkSM.js.br +0 -0
- package/build/client/_app/immutable/chunks/B6g_JkSM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{C232NBSx.js → BcH1VEVU.js} +1 -1
- package/build/client/_app/immutable/chunks/BcH1VEVU.js.br +0 -0
- package/build/client/_app/immutable/chunks/BcH1VEVU.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BPR9ibgw.js → C0bQ35Gc.js} +1 -1
- package/build/client/_app/immutable/chunks/C0bQ35Gc.js.br +0 -0
- package/build/client/_app/immutable/chunks/C0bQ35Gc.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{C9f-9-Wt.js → CEH18jSl.js} +1 -1
- package/build/client/_app/immutable/chunks/CEH18jSl.js.br +0 -0
- package/build/client/_app/immutable/chunks/CEH18jSl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CKb0QqXx.js → CSuN09OQ.js} +1 -1
- package/build/client/_app/immutable/chunks/CSuN09OQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/CSuN09OQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DUMGgJTb.js +5 -0
- package/build/client/_app/immutable/chunks/DUMGgJTb.js.br +0 -0
- package/build/client/_app/immutable/chunks/DUMGgJTb.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BAbuLHvf.js → Deds-Bu4.js} +1 -1
- package/build/client/_app/immutable/chunks/Deds-Bu4.js.br +0 -0
- package/build/client/_app/immutable/chunks/Deds-Bu4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dt0j4sZQ.js +2 -0
- package/build/client/_app/immutable/chunks/Dt0j4sZQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dt0j4sZQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{5mKQEBmA.js → DxquQJx9.js} +1 -1
- package/build/client/_app/immutable/chunks/DxquQJx9.js.br +0 -0
- package/build/client/_app/immutable/chunks/DxquQJx9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{COR6mdUG.js → V9U6b0dU.js} +1 -1
- package/build/client/_app/immutable/chunks/V9U6b0dU.js.br +0 -0
- package/build/client/_app/immutable/chunks/V9U6b0dU.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.oAXSW36_.js → app.DKvvDDfZ.js} +2 -2
- package/build/client/_app/immutable/entry/app.DKvvDDfZ.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DKvvDDfZ.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BqlKjodc.js +1 -0
- package/build/client/_app/immutable/entry/start.BqlKjodc.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BqlKjodc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.BVv0ZVNF.js → 0.CN5KvOdW.js} +1 -1
- package/build/client/_app/immutable/nodes/0.CN5KvOdW.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.CN5KvOdW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.BocieXSV.js → 1.B9DiMnGP.js} +1 -1
- package/build/client/_app/immutable/nodes/1.B9DiMnGP.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.B9DiMnGP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{15.CSwvVdKE.js → 15.DVTjsvsv.js} +1 -1
- package/build/client/_app/immutable/nodes/15.DVTjsvsv.js.br +0 -0
- package/build/client/_app/immutable/nodes/15.DVTjsvsv.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.Be6ngYER.js +35 -0
- package/build/client/_app/immutable/nodes/2.Be6ngYER.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.Be6ngYER.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.Ck1xHlrV.js → 3.CyJzwuPu.js} +1 -1
- package/build/client/_app/immutable/nodes/3.CyJzwuPu.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CyJzwuPu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.ChQxJfCA.js → 4.Ln69Bcug.js} +1 -1
- package/build/client/_app/immutable/nodes/4.Ln69Bcug.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.Ln69Bcug.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.zSWDxriP.js → 5.CvOWF9PV.js} +1 -1
- package/build/client/_app/immutable/nodes/5.CvOWF9PV.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.CvOWF9PV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.W_hIdUQV.js +2 -0
- package/build/client/_app/immutable/nodes/8.W_hIdUQV.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.W_hIdUQV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.DRayViio.js → 9.DBodeuqK.js} +1 -1
- package/build/client/_app/immutable/nodes/9.DBodeuqK.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.DBodeuqK.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-DWCTA9UP.js → 0-DRboob7o.js} +3 -3
- package/build/server/chunks/{0-DWCTA9UP.js.map → 0-DRboob7o.js.map} +1 -1
- package/build/server/chunks/{1-BQuoNKyB.js → 1-CN0hcf5T.js} +2 -2
- package/build/server/chunks/{1-BQuoNKyB.js.map → 1-CN0hcf5T.js.map} +1 -1
- package/build/server/chunks/{15-A9aJ00Nj.js → 15-BglDKcWD.js} +3 -3
- package/build/server/chunks/{15-A9aJ00Nj.js.map → 15-BglDKcWD.js.map} +1 -1
- package/build/server/chunks/{2-Dxx2KPQl.js → 2-BJlFzksb.js} +3 -3
- package/build/server/chunks/{2-Dxx2KPQl.js.map → 2-BJlFzksb.js.map} +1 -1
- package/build/server/chunks/{3-B1_UzEKm.js → 3-CyyHUkrh.js} +3 -3
- package/build/server/chunks/{3-B1_UzEKm.js.map → 3-CyyHUkrh.js.map} +1 -1
- package/build/server/chunks/{4-CDrszL6M.js → 4-C6AwWXAp.js} +3 -3
- package/build/server/chunks/{4-CDrszL6M.js.map → 4-C6AwWXAp.js.map} +1 -1
- package/build/server/chunks/5-BECI2rwz.js +9 -0
- package/build/server/chunks/{5-BZLccXTR.js.map → 5-BECI2rwz.js.map} +1 -1
- package/build/server/chunks/{8-DKi0Ar-v.js → 8-CFQq9WtG.js} +3 -3
- package/build/server/chunks/{8-DKi0Ar-v.js.map → 8-CFQq9WtG.js.map} +1 -1
- package/build/server/chunks/{9-Bm1zqhnj.js → 9-CC9KFXZB.js} +2 -2
- package/build/server/chunks/{9-Bm1zqhnj.js.map → 9-CC9KFXZB.js.map} +1 -1
- package/build/server/chunks/{AgentSidebar-DUAwX8yz.js → AgentSidebar-BHphbGfv.js} +2 -2
- package/build/server/chunks/{AgentSidebar-DUAwX8yz.js.map → AgentSidebar-BHphbGfv.js.map} +1 -1
- package/build/server/chunks/{ChatStepTrace-BraUG8oN.js → ChatStepTrace-Dy9ryvqZ.js} +18 -5
- package/build/server/chunks/ChatStepTrace-Dy9ryvqZ.js.map +1 -0
- package/build/server/chunks/{Icon-zMNuTF_-.js → Icon-C98HpfFT.js} +5 -1
- package/build/server/chunks/Icon-C98HpfFT.js.map +1 -0
- package/build/server/chunks/{_layout.svelte-D7OHpm3Z.js → _layout.svelte-BoUClC-X.js} +3 -3
- package/build/server/chunks/{_layout.svelte-D7OHpm3Z.js.map → _layout.svelte-BoUClC-X.js.map} +1 -1
- package/build/server/chunks/{_layout.svelte-DccBBHFo.js → _layout.svelte-DfPEqRLq.js} +22 -4
- package/build/server/chunks/_layout.svelte-DfPEqRLq.js.map +1 -0
- package/build/server/chunks/{_page.svelte-87WiaCHD.js → _page.svelte-CBh9OOQz.js} +3 -3
- package/build/server/chunks/{_page.svelte-87WiaCHD.js.map → _page.svelte-CBh9OOQz.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-Ct157UWV.js → _page.svelte-DR3jiCLg.js} +3 -3
- package/build/server/chunks/{_page.svelte-Ct157UWV.js.map → _page.svelte-DR3jiCLg.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-BRsvOt3E.js → _page.svelte-Dl2WmUCF.js} +3 -3
- package/build/server/chunks/{_page.svelte-BRsvOt3E.js.map → _page.svelte-Dl2WmUCF.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-CaMij-Vg.js → _page.svelte-mAhccGhM.js} +384 -94
- package/build/server/chunks/_page.svelte-mAhccGhM.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +10 -10
- package/build/server/manifest.js.map +1 -1
- package/package.json +1 -1
- package/src/backend/lib/get-agent-factories.ts +45 -1
- package/src/backend/region/router.ts +7 -20
- package/src/backend/roleplay/agents.ts +40 -13
- package/src/backend/roleplay/router.ts +158 -3
- package/src/backend/roleplay/variants.spec.ts +53 -0
- package/src/backend/roleplay/variants.ts +325 -0
- package/src/backend/tests/analyze.ts +62 -1
- package/src/backend/tests/data.ts +97 -84
- package/src/backend/tests/router.ts +19 -49
- package/src/backend/tests/suite.ts +50 -6
- package/build/client/_app/immutable/assets/0.oanZa5fp.css +0 -1
- package/build/client/_app/immutable/assets/0.oanZa5fp.css.br +0 -0
- package/build/client/_app/immutable/assets/0.oanZa5fp.css.gz +0 -0
- package/build/client/_app/immutable/chunks/5mKQEBmA.js.br +0 -0
- package/build/client/_app/immutable/chunks/5mKQEBmA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BAbuLHvf.js.br +0 -0
- package/build/client/_app/immutable/chunks/BAbuLHvf.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BH51oU1k.js +0 -2
- package/build/client/_app/immutable/chunks/BH51oU1k.js.br +0 -0
- package/build/client/_app/immutable/chunks/BH51oU1k.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BPR9ibgw.js.br +0 -0
- package/build/client/_app/immutable/chunks/BPR9ibgw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C232NBSx.js.br +0 -0
- package/build/client/_app/immutable/chunks/C232NBSx.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C9f-9-Wt.js.br +0 -0
- package/build/client/_app/immutable/chunks/C9f-9-Wt.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CKb0QqXx.js.br +0 -0
- package/build/client/_app/immutable/chunks/CKb0QqXx.js.gz +0 -0
- package/build/client/_app/immutable/chunks/COR6mdUG.js.br +0 -0
- package/build/client/_app/immutable/chunks/COR6mdUG.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CXaO9BO2.js.br +0 -0
- package/build/client/_app/immutable/chunks/CXaO9BO2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/jPRwMprt.js +0 -5
- package/build/client/_app/immutable/chunks/jPRwMprt.js.br +0 -0
- package/build/client/_app/immutable/chunks/jPRwMprt.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.oAXSW36_.js.br +0 -0
- package/build/client/_app/immutable/entry/app.oAXSW36_.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.C3H3zN_Q.js +0 -1
- package/build/client/_app/immutable/entry/start.C3H3zN_Q.js.br +0 -2
- package/build/client/_app/immutable/entry/start.C3H3zN_Q.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.BVv0ZVNF.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.BVv0ZVNF.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.BocieXSV.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.BocieXSV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/15.CSwvVdKE.js.br +0 -0
- package/build/client/_app/immutable/nodes/15.CSwvVdKE.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.IJ8N1LYx.js +0 -35
- package/build/client/_app/immutable/nodes/2.IJ8N1LYx.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.IJ8N1LYx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.Ck1xHlrV.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Ck1xHlrV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.ChQxJfCA.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.ChQxJfCA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.zSWDxriP.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.zSWDxriP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.B6qN8uAv.js +0 -2
- package/build/client/_app/immutable/nodes/8.B6qN8uAv.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.B6qN8uAv.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.DRayViio.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.DRayViio.js.gz +0 -0
- package/build/server/chunks/5-BZLccXTR.js +0 -9
- package/build/server/chunks/ChatStepTrace-BraUG8oN.js.map +0 -1
- package/build/server/chunks/Icon-zMNuTF_-.js.map +0 -1
- package/build/server/chunks/_layout.svelte-DccBBHFo.js.map +0 -1
- 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(/^```(?:
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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> {
|