fix: show agent messages before workspace lookup and bump to 1.2.24
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.2.23",
|
"version": "1.2.24",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.2.23",
|
"version": "1.2.24",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.2.23",
|
"version": "1.2.24",
|
||||||
"description": "Autonomy Studio LMS Electron app",
|
"description": "Autonomy Studio LMS Electron app",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -47,6 +47,92 @@ function harness(requestApi) {
|
|||||||
return { ...api, state, selectedModel, messageStateRef, advance: ms => { now += ms } }
|
return { ...api, state, selectedModel, messageStateRef, advance: ms => { now += ms } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('shows and persists the question, pending state and timer before a slow module snapshot finishes', async () => {
|
||||||
|
const snapshot = deferred(), completion = deferred()
|
||||||
|
const bodies = []
|
||||||
|
let loads = 0
|
||||||
|
const h = harness((_, request) => { bodies.push(JSON.parse(request.body)); return completion.promise })
|
||||||
|
const running = h.requestPlan('원격 차량 속도 제한 위치', [], [], {
|
||||||
|
environment: 'remote', workspaceTarget: 'ssh:100.64.0.10', executeTool: async () => result(),
|
||||||
|
loadInitialContext: signal => { loads++; assert.equal(signal.aborted, false); return snapshot.promise }
|
||||||
|
})
|
||||||
|
assert.equal(h.state.messages[0].content, '원격 차량 속도 제한 위치')
|
||||||
|
assert.equal(h.state.saved[0][0].role, 'user')
|
||||||
|
assert.equal(h.state.pending, true)
|
||||||
|
assert.equal(h.state.requestStartedAt, 1000)
|
||||||
|
assert.equal(h.state.activity, '모듈 상태 조회 중')
|
||||||
|
assert.equal(bodies.length, 0)
|
||||||
|
await h.requestPlan('중복 전송', [], [], { loadInitialContext: () => { loads++; return snapshot.promise } })
|
||||||
|
assert.equal(loads, 1)
|
||||||
|
assert.equal(h.state.messages.length, 1)
|
||||||
|
h.advance(5000)
|
||||||
|
h.selectedModel.provider = 'OPENAI'
|
||||||
|
snapshot.resolve({ modules: [{ name: 'Control', state: 'running', parameters: ['sim_mode:=false'], files: [] }], runtimeLogs: [] })
|
||||||
|
await tick()
|
||||||
|
assert.equal(bodies.length, 1)
|
||||||
|
assert.equal(bodies[0].provider, 'QWEN')
|
||||||
|
assert.equal(bodies[0].modules[0].state, 'running')
|
||||||
|
assert.deepEqual(bodies[0].modules[0].parameters, ['sim_mode:=false'])
|
||||||
|
h.advance(2000)
|
||||||
|
completion.resolve(response())
|
||||||
|
await running
|
||||||
|
assert.equal(h.state.messages[1].durationMs, 7000)
|
||||||
|
assert.equal(h.state.pending, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const reject of [false, true]) test(`cancels during initial module lookup and ignores its late ${reject ? 'failure' : 'result'} without affecting a new request`, async () => {
|
||||||
|
const old = deferred(), next = deferred()
|
||||||
|
let calls = 0, signal
|
||||||
|
const h = harness(async () => { calls++; return response() })
|
||||||
|
const first = h.requestPlan('첫 질문', [], [], { loadInitialContext: current => { signal = current; return old.promise } })
|
||||||
|
h.stop()
|
||||||
|
assert.equal(signal.aborted, true)
|
||||||
|
assert.equal(h.state.pending, false)
|
||||||
|
const second = h.requestPlan('다음 질문', [], [], { loadInitialContext: () => next.promise })
|
||||||
|
if (reject) old.reject(Error('늦게 도착한 SSH 오류'))
|
||||||
|
else old.resolve({ modules: [], runtimeLogs: [] })
|
||||||
|
await first
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(h.state.pending, true)
|
||||||
|
assert.equal(h.state.activity, '모듈 상태 조회 중')
|
||||||
|
assert.equal(h.state.messages.length, 2)
|
||||||
|
assert.equal(h.state.notices, 0)
|
||||||
|
next.resolve({ modules: [], runtimeLogs: [] })
|
||||||
|
await second
|
||||||
|
assert.equal(calls, 1)
|
||||||
|
assert.equal(h.state.messages.length, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('initial snapshot errors preserve the question and show a timed error without calling the model', async () => {
|
||||||
|
const snapshot = deferred()
|
||||||
|
let calls = 0
|
||||||
|
const h = harness(async () => { calls++; return response() })
|
||||||
|
const running = h.requestPlan('질문', [], [], { loadInitialContext: () => snapshot.promise })
|
||||||
|
h.advance(3000)
|
||||||
|
snapshot.reject(Error('원격 모듈 상태 조회 실패'))
|
||||||
|
await running
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(h.state.messages[0].content, '질문')
|
||||||
|
assert.equal(h.state.messages[1].content, '원격 모듈 상태 조회 실패')
|
||||||
|
assert.equal(h.state.messages[1].durationMs, 3000)
|
||||||
|
assert.equal(h.state.pending, false)
|
||||||
|
assert.equal(h.state.requestStartedAt, null)
|
||||||
|
assert.equal(h.state.activity, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('scope changes during initial snapshot loading suppress later API calls and errors', async () => {
|
||||||
|
const snapshot = deferred()
|
||||||
|
let calls = 0
|
||||||
|
const h = harness(async () => { calls++; return response() })
|
||||||
|
const running = h.requestPlan('이전 강좌 질문', [], [], { loadInitialContext: () => snapshot.promise })
|
||||||
|
h.messageStateRef.current.session = {}
|
||||||
|
snapshot.resolve({ modules: [], runtimeLogs: [] })
|
||||||
|
await running
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(h.state.messages.length, 1)
|
||||||
|
assert.equal(h.state.notices, 0)
|
||||||
|
})
|
||||||
|
|
||||||
test('publishes completed stages immediately without persisting progress or adding model calls', async () => {
|
test('publishes completed stages immediately without persisting progress or adding model calls', async () => {
|
||||||
const first = deferred(), tool = deferred(), final = deferred()
|
const first = deferred(), tool = deferred(), final = deferred()
|
||||||
let calls = 0
|
let calls = 0
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const plain = value => JSON.parse(JSON.stringify(value))
|
|||||||
function harness(remoteWorkspace) {
|
function harness(remoteWorkspace) {
|
||||||
const executions = []
|
const executions = []
|
||||||
const calls = []
|
const calls = []
|
||||||
|
const contexts = []
|
||||||
|
const events = []
|
||||||
const errors = []
|
const errors = []
|
||||||
const target = { host: '100.64.0.10', user: 'fixture', workspaceRoot: '/fixture/workspace' }
|
const target = { host: '100.64.0.10', user: 'fixture', workspaceRoot: '/fixture/workspace' }
|
||||||
const live = [{ name: 'NAV2-MKMini', folderName: 'vehicle', state: 'running', parameters: ['sim_mode:=false', 'robot:=mkmini'] }]
|
const live = [{ name: 'NAV2-MKMini', folderName: 'vehicle', state: 'running', parameters: ['sim_mode:=false', 'robot:=mkmini'] }]
|
||||||
@@ -26,10 +28,14 @@ function harness(remoteWorkspace) {
|
|||||||
getRemoteWorkspaceTarget: () => target, applyModuleParameterOverrides: modules => modules,
|
getRemoteWorkspaceTarget: () => target, applyModuleParameterOverrides: modules => modules,
|
||||||
validateAgentTool: request => request, AGENT_PRACTICE_PROFILES: [],
|
validateAgentTool: request => request, AGENT_PRACTICE_PROFILES: [],
|
||||||
window: {
|
window: {
|
||||||
remoteWorkspace: { modules: { list: async () => ({ modules: live }) }, agent: { readTool: readTool('remote') } },
|
remoteWorkspace: { modules: { list: async () => { events.push('snapshot-read'); return { modules: live } } }, agent: { readTool: readTool('remote') } },
|
||||||
localWorkspace: { modules: { list: async () => ({ modules: live }) }, agent: { readTool: readTool('local') } }
|
localWorkspace: { modules: { list: async () => { events.push('snapshot-read'); return { modules: live } } }, agent: { readTool: readTool('local') } }
|
||||||
|
},
|
||||||
|
requestAgentPlan: async (...args) => {
|
||||||
|
calls.push(args)
|
||||||
|
events.push('plan-start')
|
||||||
|
contexts.push(await args[3].loadInitialContext(new AbortController().signal))
|
||||||
},
|
},
|
||||||
requestAgentPlan: async (...args) => { calls.push(args) },
|
|
||||||
onTitlebarNotice: (...args) => { errors.push(args) }, console: { error: () => {} },
|
onTitlebarNotice: (...args) => { errors.push(args) }, console: { error: () => {} },
|
||||||
loadAgentRequestedContext: async () => ({ modules: [], runtimeLogs: [] })
|
loadAgentRequestedContext: async () => ({ modules: [], runtimeLogs: [] })
|
||||||
}
|
}
|
||||||
@@ -37,16 +43,19 @@ function harness(remoteWorkspace) {
|
|||||||
vm.runInNewContext(ts.transpileModule(names.map(name => `const ${name} = ${declarations.get(name)}; exports.${name} = ${name}`).join('\n'), {
|
vm.runInNewContext(ts.transpileModule(names.map(name => `const ${name} = ${declarations.get(name)}; exports.${name} = ${name}`).join('\n'), {
|
||||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
||||||
}).outputText, globals)
|
}).outputText, globals)
|
||||||
return { ...exports, executions, calls, errors }
|
return { ...exports, executions, calls, contexts, events, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const remote of [false, true]) {
|
for (const remote of [false, true]) {
|
||||||
test(`${remote ? 'remote' : 'local'}: a new question refreshes state and start settings before sending context`, async () => {
|
test(`${remote ? 'remote' : 'local'}: enters the chat lifecycle immediately and refreshes state before the provider request`, async () => {
|
||||||
const h = harness(remote)
|
const h = harness(remote)
|
||||||
await h.submitAgentRequest('최대 속도 제한 코드 위치')
|
await h.submitAgentRequest('최대 속도 제한 코드 위치')
|
||||||
assert.deepEqual(h.errors, [])
|
assert.deepEqual(h.errors, [])
|
||||||
assert.equal(h.calls.length, 1)
|
assert.equal(h.calls.length, 1)
|
||||||
const [, modules, , options] = h.calls[0]
|
const [, initialModules, , options] = h.calls[0]
|
||||||
|
assert.deepEqual(plain(initialModules), [])
|
||||||
|
assert.deepEqual(h.events, ['plan-start', 'snapshot-read'])
|
||||||
|
const { modules } = h.contexts[0]
|
||||||
assert.equal(modules[0].state, 'running')
|
assert.equal(modules[0].state, 'running')
|
||||||
assert.deepEqual(plain(modules[0].parameters), ['sim_mode:=false', 'robot:=mkmini'])
|
assert.deepEqual(plain(modules[0].parameters), ['sim_mode:=false', 'robot:=mkmini'])
|
||||||
assert.deepEqual(plain(modules[0].files), [])
|
assert.deepEqual(plain(modules[0].files), [])
|
||||||
|
|||||||
@@ -3679,9 +3679,13 @@ function LocalPcWorkspaceMockup({
|
|||||||
const submitAgentRequest = async (content: string, attachments: ChatAttachment[] = []): Promise<void> => {
|
const submitAgentRequest = async (content: string, attachments: ChatAttachment[] = []): Promise<void> => {
|
||||||
if (agentActionControllerRef.current || agentApplying) return
|
if (agentActionControllerRef.current || agentApplying) return
|
||||||
try {
|
try {
|
||||||
const modules = await collectAgentWorkspace([], await readAgentModules())
|
await requestAgentPlan(content, [], [], {
|
||||||
await requestAgentPlan(content, modules, [], {
|
|
||||||
attachments,
|
attachments,
|
||||||
|
loadInitialContext: async (signal) => {
|
||||||
|
const modules = await readAgentModules()
|
||||||
|
if (signal.aborted) throw new DOMException('Aborted', 'AbortError')
|
||||||
|
return { modules: await collectAgentWorkspace([], modules), runtimeLogs: [] }
|
||||||
|
},
|
||||||
loadRequestedContext: loadAgentRequestedContext,
|
loadRequestedContext: loadAgentRequestedContext,
|
||||||
executeTool: executeWorkspaceAgentTool,
|
executeTool: executeWorkspaceAgentTool,
|
||||||
environment: remoteWorkspace ? 'remote' : 'local',
|
environment: remoteWorkspace ? 'remote' : 'local',
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
|||||||
options: {
|
options: {
|
||||||
showUserMessage?: boolean
|
showUserMessage?: boolean
|
||||||
attachments?: ChatAttachment[]
|
attachments?: ChatAttachment[]
|
||||||
|
loadInitialContext?: (signal: AbortSignal) => Promise<AgentContext>
|
||||||
loadRequestedContext?: (request: AgentContextRequest) => Promise<AgentContext>
|
loadRequestedContext?: (request: AgentContextRequest) => Promise<AgentContext>
|
||||||
executeTool?: (request: AgentToolRequest) => Promise<AgentToolResult>
|
executeTool?: (request: AgentToolRequest) => Promise<AgentToolResult>
|
||||||
environment?: 'local' | 'remote'
|
environment?: 'local' | 'remote'
|
||||||
@@ -346,6 +347,13 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let initialContext: AgentContext = { modules, runtimeLogs, ...(options.toolResults?.length ? { toolResults: options.toolResults } : {}) }
|
||||||
|
if (options.loadInitialContext) {
|
||||||
|
setActivity('모듈 상태 조회 중')
|
||||||
|
const loaded = await options.loadInitialContext(abortController.signal)
|
||||||
|
if (!currentRequest()) return
|
||||||
|
initialContext = { ...initialContext, ...loaded }
|
||||||
|
}
|
||||||
const textAttachments = (options.attachments ?? []).flatMap(file => file.kind === 'image' ? [] : [{ name: file.name, content: file.content }])
|
const textAttachments = (options.attachments ?? []).flatMap(file => file.kind === 'image' ? [] : [{ name: file.name, content: file.content }])
|
||||||
const imageAttachments: AgentImageReference[] = []
|
const imageAttachments: AgentImageReference[] = []
|
||||||
for (const file of options.attachments ?? []) {
|
for (const file of options.attachments ?? []) {
|
||||||
@@ -358,15 +366,15 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
|||||||
if (Number.isInteger(weekNumber) && weekNumber >= 1) body.append('week', String(weekNumber))
|
if (Number.isInteger(weekNumber) && weekNumber >= 1) body.append('week', String(weekNumber))
|
||||||
body.append('file', file.file, file.name)
|
body.append('file', file.file, file.name)
|
||||||
const uploaded = await requestApi<AgentImageReference>('/chat/agent/attachments/images', { method: 'POST', body, signal: abortController.signal })
|
const uploaded = await requestApi<AgentImageReference>('/chat/agent/attachments/images', { method: 'POST', body, signal: abortController.signal })
|
||||||
|
if (!currentRequest()) return
|
||||||
imageAttachments.push(uploaded)
|
imageAttachments.push(uploaded)
|
||||||
if (options.showUserMessage !== false) updateMessages(current => current.map(item => item.id === userMessage.id ? { ...item, imageAttachments: [...imageAttachments] } : item))
|
if (options.showUserMessage !== false) updateMessages(current => current.map(item => item.id === userMessage.id ? { ...item, imageAttachments: [...imageAttachments] } : item))
|
||||||
if (abortController.signal.aborted) return
|
|
||||||
appendStep({ id: `image-${imageAttachments.length}`, kind: 'image', status: 'ok', name: file.name })
|
appendStep({ id: `image-${imageAttachments.length}`, kind: 'image', status: 'ok', name: file.name })
|
||||||
}
|
}
|
||||||
const imageTokens = imageAttachments.map(image => image.token)
|
const imageTokens = imageAttachments.map(image => image.token)
|
||||||
setActivity('요청 분석 중')
|
setActivity('요청 분석 중')
|
||||||
const response = await resolveAgentContext({
|
const response = await resolveAgentContext({
|
||||||
initialContext: { modules, runtimeLogs, ...(options.toolResults?.length ? { toolResults: options.toolResults } : {}) },
|
initialContext,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
loadRequestedContext: options.loadRequestedContext,
|
loadRequestedContext: options.loadRequestedContext,
|
||||||
executeTool: options.executeTool,
|
executeTool: options.executeTool,
|
||||||
@@ -378,7 +386,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
|||||||
body: JSON.stringify({ courseId, week: weekNumber, ...selection, request: agentPlanRequestText(request, finalize), ...(options.executeTool ? { toolProtocol: 1, environment: options.environment, workspaceTarget: options.workspaceTarget } : {}), ...(textAttachments.length ? { attachments: textAttachments } : {}), ...(imageTokens.length ? { imageTokens } : {}), ...context })
|
body: JSON.stringify({ courseId, week: weekNumber, ...selection, request: agentPlanRequestText(request, finalize), ...(options.executeTool ? { toolProtocol: 1, environment: options.environment, workspaceTarget: options.workspaceTarget } : {}), ...(textAttachments.length ? { attachments: textAttachments } : {}), ...(imageTokens.length ? { imageTokens } : {}), ...context })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (abortController.signal.aborted) return
|
if (!currentRequest()) return
|
||||||
const plan: AgentPlan = {
|
const plan: AgentPlan = {
|
||||||
...response.plan,
|
...response.plan,
|
||||||
requestedModules: response.plan.requestedModules ?? [],
|
requestedModules: response.plan.requestedModules ?? [],
|
||||||
@@ -402,7 +410,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
|||||||
}])
|
}])
|
||||||
onResponseRef.current?.()
|
onResponseRef.current?.()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (abortController.signal.aborted) return
|
if (!currentRequest()) return
|
||||||
updateMessages((current) => [...current, {
|
updateMessages((current) => [...current, {
|
||||||
id: `agent-error-${Date.now()}`,
|
id: `agent-error-${Date.now()}`,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
|
|||||||
@@ -741,6 +741,7 @@
|
|||||||
"소스 파일": "Source files",
|
"소스 파일": "Source files",
|
||||||
"{{count}}건": "{{count}} items",
|
"{{count}}건": "{{count}} items",
|
||||||
"요청 분석 중": "Analyzing request",
|
"요청 분석 중": "Analyzing request",
|
||||||
|
"모듈 상태 조회 중": "Checking module status",
|
||||||
"조회 결과 분석 중": "Analyzing retrieved results",
|
"조회 결과 분석 중": "Analyzing retrieved results",
|
||||||
"추가 자료 조회 중": "Retrieving additional context",
|
"추가 자료 조회 중": "Retrieving additional context",
|
||||||
"최종 답변 정리 중": "Preparing the final answer",
|
"최종 답변 정리 중": "Preparing the final answer",
|
||||||
|
|||||||
Reference in New Issue
Block a user