|
@@ -33,6 +33,7 @@ let requests: Array<{ method: string; params: Record<string, any> }>,
|
|
|
let nodes: WorkspaceNodes
|
|
let nodes: WorkspaceNodes
|
|
|
let configuredCwds: Map<string, string>
|
|
let configuredCwds: Map<string, string>
|
|
|
let rejectPrompt: string | undefined
|
|
let rejectPrompt: string | undefined
|
|
|
|
|
+let observeGatewayRequest: ((frame: { method: string; params: Record<string, any> }) => void) | undefined
|
|
|
let rejectedModelConfig: Record<string, unknown> | undefined
|
|
let rejectedModelConfig: Record<string, unknown> | undefined
|
|
|
let rejectedInterrupts: number
|
|
let rejectedInterrupts: number
|
|
|
let recoveryHistory: Array<{ role: string; content: string; tool_calls?: unknown }>
|
|
let recoveryHistory: Array<{ role: string; content: string; tool_calls?: unknown }>
|
|
@@ -48,6 +49,7 @@ beforeEach(async () => {
|
|
|
recoveryHistory = []
|
|
recoveryHistory = []
|
|
|
storedByRuntime = new Map(); recoveryByStored = new Map()
|
|
storedByRuntime = new Map(); recoveryByStored = new Map()
|
|
|
rejectPrompt = undefined
|
|
rejectPrompt = undefined
|
|
|
|
|
+ observeGatewayRequest = undefined
|
|
|
rejectedModelConfig = undefined
|
|
rejectedModelConfig = undefined
|
|
|
rejectedInterrupts = 0
|
|
rejectedInterrupts = 0
|
|
|
server = new WebSocketServer({ port: 0, host: '127.0.0.1' })
|
|
server = new WebSocketServer({ port: 0, host: '127.0.0.1' })
|
|
@@ -88,6 +90,7 @@ beforeEach(async () => {
|
|
|
socket.on('message', (data) => {
|
|
socket.on('message', (data) => {
|
|
|
const frame = JSON.parse(String(data))
|
|
const frame = JSON.parse(String(data))
|
|
|
requests.push(frame)
|
|
requests.push(frame)
|
|
|
|
|
+ observeGatewayRequest?.(frame)
|
|
|
const respond = (result: unknown) => socket.send(JSON.stringify({ id: frame.id, result }))
|
|
const respond = (result: unknown) => socket.send(JSON.stringify({ id: frame.id, result }))
|
|
|
if (frame.method === 'session.create' || frame.method === 'session.resume') {
|
|
if (frame.method === 'session.create' || frame.method === 'session.resume') {
|
|
|
const runtimeId = randomUUID(), storedId = frame.method === 'session.resume' ? frame.params.session_id : randomUUID()
|
|
const runtimeId = randomUUID(), storedId = frame.method === 'session.resume' ? frame.params.session_id : randomUUID()
|
|
@@ -1801,3 +1804,49 @@ it('does not send a prompt when the model bridge cannot resolve the saved settin
|
|
|
await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('failed'))
|
|
await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('failed'))
|
|
|
expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(0)
|
|
expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(0)
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+it('applies Bot settings to the live agent after a cold resume restores its persisted runtime during tool binding', async () => {
|
|
|
|
|
+ modelBridge()
|
|
|
|
|
+ const a = agent('冷启动模型切换'), c = direct(a.id)
|
|
|
|
|
+ const target = nodes.target(owner, 'local'), original = target.session.request.bind(target.session)
|
|
|
|
|
+ const models = new Map<string, Record<string, string>>()
|
|
|
|
|
+ const submitted: Array<Record<string, string>> = []
|
|
|
|
|
+ target.session.request = async (path, options) => {
|
|
|
|
|
+ if (path.endsWith('/capabilities')) return { status: 200, headers: new Headers(), body: Buffer.from(JSON.stringify({ version: 1, ready: true, in_process: true, native_tools: true })) }
|
|
|
|
|
+ if (path.endsWith('/bind')) {
|
|
|
|
|
+ // Hermes acknowledges config.set before a deferred resume builds the agent.
|
|
|
|
|
+ // That build restores the stored runtime, replacing any premature settings.
|
|
|
|
|
+ const sid = (options?.body as any).session_id
|
|
|
|
|
+ models.set(sid, { model: 'persisted-model', reasoning: 'medium', fast: 'normal' })
|
|
|
|
|
+ return { status: 200, headers: new Headers(), body: Buffer.from(JSON.stringify({ ok: true, native_tools: true })) }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (path.endsWith('/unbind')) return { status: 200, headers: new Headers(), body: Buffer.from('{"ok":true}') }
|
|
|
|
|
+ return original(path, options)
|
|
|
|
|
+ }
|
|
|
|
|
+ observeGatewayRequest = frame => {
|
|
|
|
|
+ if (frame.method !== 'config.set') return
|
|
|
|
|
+ const sid = frame.params.session_id, current = models.get(sid) ?? {}
|
|
|
|
|
+ current[frame.params.key] = frame.params.value
|
|
|
|
|
+ models.set(sid, current)
|
|
|
|
|
+ }
|
|
|
|
|
+ reply = (socket, params) => {
|
|
|
|
|
+ submitted.push({ ...models.get(params.session_id) })
|
|
|
|
|
+ socket.send(JSON.stringify({ method: 'event', params: { type: 'message.complete', session_id: params.session_id, payload: { text: '完成', status: 'complete' } } }))
|
|
|
|
|
+ }
|
|
|
|
|
+ configuredCwds.set('default', '/profile/working-directory')
|
|
|
|
|
+ for (const settings of [
|
|
|
|
|
+ { provider: 'openai', model: 'model-a', reasoningEffort: 'high', fastMode: 'fast' },
|
|
|
|
|
+ { provider: 'custom:provider', model: 'model-b', reasoningEffort: 'none', fastMode: 'cold' },
|
|
|
|
|
+ null,
|
|
|
|
|
+ ]) {
|
|
|
|
|
+ store.updateAgent(owner, a.id, { modelSettings: settings })
|
|
|
|
|
+ await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '切换模型并保留上下文' }).id)
|
|
|
|
|
+ }
|
|
|
|
|
+ expect(submitted).toEqual([
|
|
|
|
|
+ { model: 'model-a --provider openai --session', reasoning: 'high', fast: 'fast' },
|
|
|
|
|
+ { model: 'model-b --provider custom:provider --session', reasoning: 'none', fast: 'cold' },
|
|
|
|
|
+ { model: 'base-model --provider base --session', reasoning: 'medium', fast: 'normal' },
|
|
|
|
|
+ ])
|
|
|
|
|
+ expect(requests.filter(r => r.method === 'session.resume')).toHaveLength(2)
|
|
|
|
|
+})
|