feat: improve agent model selection and remote source analysis; bump to 1.2.23

This commit is contained in:
2026-10-20 11:36:28 +09:00
parent cb5872c326
commit a8dd23675a
22 changed files with 711 additions and 70 deletions
+62 -5
View File
@@ -11,7 +11,10 @@ function compile(source, globals = {}) {
}).outputText, { exports, DOMException, Error, ...globals })
return exports
}
const context = compile(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentContext.ts'), 'utf8'))
const evidenceApi = compile(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentEvidence.ts'), 'utf8'))
const context = compile(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentContext.ts'), 'utf8'), {
require: name => { assert.equal(name, './agentEvidence'); return evidenceApi }
})
const source = ts.createSourceFile('useAiAgent.ts', fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/useAiAgent.ts'), 'utf8'), ts.ScriptTarget.Latest, true)
const hook = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === 'useAiAgent')
const functions = hook.body.statements.filter(node => ts.isVariableStatement(node) && ['requestPlan', 'stop'].includes(node.declarationList.declarations[0].name.getText(source)))
@@ -27,11 +30,13 @@ function harness(requestApi) {
class Clock extends Date { static now() { return now } }
const state = { pending: false, activity: '', steps: [], messages: [], saved: [], notices: 0, requestStartedAt: null }
const messageStateRef = { current: { session } }
const selectedModel = { provider: 'QWEN', model: 'fixture', imagesEnabled: true }
const api = compile(functions.map(node => `export ${node.getText(source)}`).join('\n'), {
...context, session, messageStateRef, AbortController,
...context, session, messageStateRef, AbortController, FormData,
Date: Clock,
courseId: 'course', weekNumber: 3, pending: false, historyLoading: false,
status: { enabled: true }, abortControllerRef: { current: null },
courseId: 'course', weekNumber: 3, pending: false, historyLoading: false, statusLoading: false,
selectedModel,
status: { enabled: true, selectionSupported: true }, abortControllerRef: { current: null },
setPending: value => { state.pending = value },
setRequestStartedAt: value => { state.requestStartedAt = value },
setActivity: value => { state.activity = value },
@@ -39,7 +44,7 @@ function harness(requestApi) {
updateMessages: update => { state.messages = update(state.messages); state.saved.push(plain(state.messages)) },
onResponseRef: { current: () => { state.notices++ } }, requestApi
})
return { ...api, state, messageStateRef, advance: ms => { now += ms } }
return { ...api, state, selectedModel, messageStateRef, advance: ms => { now += ms } }
}
test('publishes completed stages immediately without persisting progress or adding model calls', async () => {
@@ -76,6 +81,58 @@ test('publishes completed stages immediately without persisting progress or addi
assert.equal(h.state.notices, 1)
})
test('image uploads and every lookup round retain the original provider without uploading the image twice', async () => {
const bodies = []
let uploads = 0
const h = harness(async (path, request) => {
if (path.endsWith('/attachments/images')) {
uploads++
assert.equal(request.body.get('provider'), 'QWEN')
assert.equal(request.body.get('courseId'), 'course')
assert.equal(request.body.get('week'), '3')
assert.equal(request.body.get('file').name, 'screen.png')
return { token: 'image-reference', name: 'screen.png', url: 'https://images.test/screen.png' }
}
bodies.push(JSON.parse(request.body))
h.selectedModel.provider = 'OPENAI'
return bodies.length === 1
? { ...response(), plan: { ...response().plan, requestedModules: ['Control'] } }
: response()
})
await h.requestPlan('이미지 확인', [{ name: 'Control', files: [] }], [], {
attachments: [{ kind: 'image', name: 'screen.png', file: new Blob(['fixture'], { type: 'image/png' }) }],
loadRequestedContext: async () => ({ modules: [{ name: 'Control', files: [{ path: 'config/control.yaml', content: 'speed: 0.3' }] }], runtimeLogs: [] })
})
assert.equal(uploads, 1)
assert.equal(bodies.length, 2)
assert.ok(bodies.every(body => body.provider === 'QWEN'))
assert.ok(bodies.every(body => body.imageTokens[0] === 'image-reference'))
assert.equal(h.state.messages.at(-1).model, 'fixture')
})
test('remote target and observed source ranges reach each provider request including finalization', async () => {
const bodies = []
const tool = { id: 'read', name: 'files.read', moduleName: 'Control', args: { path: 'config/car.yaml', startLine: 20, lineCount: 1 } }
const h = harness(async (_, request) => {
bodies.push(JSON.parse(request.body))
return bodies.length < 3 ? response([tool]) : response()
})
await h.requestPlan('속도 제한 코드 위치', [{ name: 'Control', state: 'running', parameters: ['sim_mode:=false'], files: [] }], [], {
environment: 'remote', workspaceTarget: 'ssh:100.64.0.10',
executeTool: async input => result({ id: input.id, moduleName: 'Control', target: 'ssh:100.64.0.10', truncated: true,
data: JSON.stringify({ path: 'config/car.yaml', exists: true, hash: 'a'.repeat(64), complete: false, content: 'max_speed: 0.4', startLine: 20, endLine: 20, contentTruncated: false, totalLines: 100 }) })
})
assert.equal(bodies.length, 3)
for (const body of bodies) {
assert.equal(body.environment, 'remote')
assert.equal(body.workspaceTarget, 'ssh:100.64.0.10')
assert.deepEqual(body.modules[0].parameters, ['sim_mode:=false'])
}
assert.equal(JSON.parse(bodies[1].toolResults[0].data).content, 'max_speed: 0.4')
assert.equal(JSON.parse(bodies[2].toolResults[0].data).endLine, 20)
assert.match(bodies[2].request, /자료 조회는 종료되었습니다/)
})
test('errors replace temporary progress with the existing error reply', async () => {
let calls = 0
const h = harness(async () => {