feat: preserve compact agent context and bump to 1.2.25
This commit is contained in:
@@ -3689,7 +3689,8 @@ function LocalPcWorkspaceMockup({
|
||||
loadRequestedContext: loadAgentRequestedContext,
|
||||
executeTool: executeWorkspaceAgentTool,
|
||||
environment: remoteWorkspace ? 'remote' : 'local',
|
||||
workspaceTarget: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host'
|
||||
workspaceTarget: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host',
|
||||
workspaceRoot: remoteWorkspace ? JSON.stringify([getRemoteWorkspaceTarget().user, getRemoteWorkspaceTarget().port, getRemoteWorkspaceTarget().workspaceRoot]) : clonePathPreview
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[agent] workspace collection failed', error)
|
||||
@@ -3831,7 +3832,9 @@ function LocalPcWorkspaceMockup({
|
||||
const currentModules = await readAgentModules()
|
||||
assertActive()
|
||||
await requestAgentPlan(`학생의 원래 요청: ${originalRequest.slice(0, 2600)}\n승인 작업 ${command.command}의 실행 결과가 workspace.toolResults에 있습니다. 결과를 분석하고 확인된 성공과 실패를 설명하세요. 이미 승인·실행한 작업을 반복하지 마세요.`, currentModules.map(item => ({ name: item.name, state: item.state, category: item.category, parameters: item.parameters, files: [] })), [], {
|
||||
showUserMessage: false, environment, workspaceTarget: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host', executeTool: executeWorkspaceAgentTool, loadRequestedContext: loadAgentRequestedContext,
|
||||
showUserMessage: false, environment, workspaceTarget: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host',
|
||||
workspaceRoot: remoteWorkspace ? JSON.stringify([getRemoteWorkspaceTarget().user, getRemoteWorkspaceTarget().port, getRemoteWorkspaceTarget().workspaceRoot]) : clonePathPreview,
|
||||
executeTool: executeWorkspaceAgentTool, loadRequestedContext: loadAgentRequestedContext,
|
||||
toolResults: [{ id: `action-${Date.now()}`, name: 'action.result', moduleName: module.name, observedAt: new Date().toISOString(), target: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host', status: ok ? 'ok' : 'error', data: JSON.stringify({ approvedAction: operation, result: output }), truncated: false }]
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -3914,7 +3917,8 @@ function LocalPcWorkspaceMockup({
|
||||
loadRequestedContext: loadAgentRequestedContext,
|
||||
executeTool: executeWorkspaceAgentTool,
|
||||
environment: remoteWorkspace ? 'remote' : 'local',
|
||||
workspaceTarget: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host'
|
||||
workspaceTarget: remoteWorkspace ? `ssh:${getRemoteWorkspaceTarget().host}` : 'local-host',
|
||||
workspaceRoot: remoteWorkspace ? JSON.stringify([getRemoteWorkspaceTarget().user, getRemoteWorkspaceTarget().port, getRemoteWorkspaceTarget().workspaceRoot]) : clonePathPreview
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = (error instanceof Error ? error.message : '명령을 실행하지 못했습니다.')
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { AgentConversationMessage } from './useAiAgent'
|
||||
|
||||
export type AgentConversationScope = { workspace: string; createdAt: number }
|
||||
export type AgentConversationContext = {
|
||||
environment: 'local' | 'remote'
|
||||
workspaceTarget: string
|
||||
summary: string
|
||||
recentMessages: Array<{ role: 'user' | 'assistant'; content: string }>
|
||||
}
|
||||
|
||||
const MAX_AGE_MS = 24 * 60 * 60 * 1000
|
||||
const RECENT_TURNS = 2
|
||||
const MESSAGE_LIMIT = 700
|
||||
const SUMMARY_LIMIT = 1000
|
||||
|
||||
const clip = (text: string, limit: number): string => text.length > limit ? `${text.slice(0, limit - 3)}...` : text
|
||||
|
||||
export function agentConversationWorkspace(environment?: 'local' | 'remote', target?: string, root?: string): string | undefined {
|
||||
if (!environment || !target || !root) return undefined
|
||||
const workspace = JSON.stringify([environment, target, root])
|
||||
return workspace.length <= 1000 ? workspace : undefined
|
||||
}
|
||||
|
||||
function responseContext(message: AgentConversationMessage, limit: number): string {
|
||||
const plan = message.plan!
|
||||
// Keep identities and outcomes, never replay source contents, logs, or executable payloads.
|
||||
const details = [
|
||||
...(plan.codeReferences ?? []).slice(0, 3).map(item => `${item.moduleName}/${item.path}:${item.startLine}-${item.endLine}`),
|
||||
...plan.changes.slice(0, 2).map(item => `${item.moduleName}/${item.path} (file change: ${message.applyState ?? 'ready'})`),
|
||||
...plan.commands.slice(0, 2).map(item => `${item.moduleName} (${clip(item.purpose, 60)}: ${message.commandExecutions?.[item.id]?.status ?? 'pending'})`)
|
||||
].join('; ')
|
||||
const targets = details ? `\nHistorical references/outcomes: ${clip(details, Math.min(240, Math.floor(limit / 2)))}` : ''
|
||||
const answer = (limit > 300 ? `${plan.summary}\n${plan.explanation}` : plan.summary).replace(/```[^\n]*\n[\s\S]*?(?:```|$)/g, '[code block omitted]')
|
||||
return `${clip(answer.trim(), limit - targets.length)}${targets}`
|
||||
}
|
||||
|
||||
export function buildAgentConversationContext(
|
||||
messages: AgentConversationMessage[],
|
||||
workspace: string | undefined,
|
||||
now = Date.now()
|
||||
): AgentConversationContext | undefined {
|
||||
if (!workspace) return undefined
|
||||
const [environment, workspaceTarget] = JSON.parse(workspace) as ['local' | 'remote', string, string]
|
||||
const turns: Array<{ user: AgentConversationMessage; assistant: AgentConversationMessage }> = []
|
||||
let summary = ''
|
||||
let user: AgentConversationMessage | undefined
|
||||
const inScope = (message: AgentConversationMessage): boolean => message.conversationScope?.workspace === workspace
|
||||
&& Number.isFinite(message.conversationScope.createdAt)
|
||||
&& message.conversationScope.createdAt <= now
|
||||
&& now - message.conversationScope.createdAt < MAX_AGE_MS
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === 'user') {
|
||||
user = inScope(message) ? message : undefined
|
||||
continue
|
||||
}
|
||||
if (!user || !inScope(message) || !message.plan || message.plan.toolRequests?.length
|
||||
|| message.plan.requestedModules.length || message.plan.requestedLogs.length) continue
|
||||
const previous = turns.at(-1)
|
||||
if (previous?.user === user) previous.assistant = message
|
||||
else turns.push({ user, assistant: message })
|
||||
if (turns.length > RECENT_TURNS) turns.shift()
|
||||
if (message.plan.conversationSummary?.trim()) summary = clip(message.plan.conversationSummary.trim(), SUMMARY_LIMIT)
|
||||
}
|
||||
if (!turns.length) return undefined
|
||||
const recent = turns.slice(-RECENT_TURNS)
|
||||
return {
|
||||
environment, workspaceTarget, summary,
|
||||
recentMessages: recent.flatMap(turn => [
|
||||
{ role: 'user' as const, content: clip(turn.user.content, MESSAGE_LIMIT) },
|
||||
{ role: 'assistant' as const, content: responseContext(turn.assistant, MESSAGE_LIMIT) }
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { agentPlanRequestText, recoverInterruptedAgentMessages, resolveAgentCont
|
||||
import type { AgentImageReference, ChatAttachment } from '../../components/chatAttachments'
|
||||
import type { AgentOperation, AgentToolRequest, AgentToolResult } from '../../../../shared/agentTools'
|
||||
import { normalizeAgentStatus, selectedAgentModel, type AgentStatus } from './agentModels'
|
||||
import { agentConversationWorkspace, buildAgentConversationContext, type AgentConversationScope } from './agentConversationContext'
|
||||
|
||||
export type AgentFileChange = {
|
||||
expectedHash?: string | null
|
||||
@@ -54,6 +55,7 @@ export type AgentCommandExecution = {
|
||||
}
|
||||
|
||||
export type AgentPlan = {
|
||||
conversationSummary?: string
|
||||
codeReferences?: AgentCodeReference[]
|
||||
toolRequests?: AgentToolRequest[]
|
||||
evidence?: AgentToolResult[]
|
||||
@@ -88,6 +90,7 @@ export type AgentRuntimeLog = {
|
||||
}
|
||||
|
||||
export type AgentConversationMessage = {
|
||||
conversationScope?: AgentConversationScope
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
@@ -177,7 +180,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
||||
if (response.status === 400 && message?.split('\n').includes('property attachments should not exist')) {
|
||||
throw new Error('연결된 서버가 에이전트 파일 첨부를 지원하지 않습니다. 백엔드를 업데이트한 뒤 다시 시도하거나 첨부 없이 질문해 주세요.')
|
||||
}
|
||||
if (response.status === 400 && /property (?:provider|toolProtocol|toolResults|environment|workspaceTarget|contextNotes|imageTokens) should not exist/.test(message ?? '')) throw new Error('연결된 서버가 에이전트 요청 형식을 지원하지 않습니다. 백엔드를 먼저 업데이트해주세요.')
|
||||
if (response.status === 400 && /property (?:provider|toolProtocol|toolResults|environment|workspaceTarget|contextNotes|imageTokens|conversationContext) should not exist/.test(message ?? '')) throw new Error('연결된 서버가 에이전트 요청 형식을 지원하지 않습니다. 백엔드를 먼저 업데이트해주세요.')
|
||||
throw new Error(message ?? 'AI 에이전트 요청을 처리하지 못했습니다.')
|
||||
}
|
||||
return response.json() as Promise<T>
|
||||
@@ -321,6 +324,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
||||
executeTool?: (request: AgentToolRequest) => Promise<AgentToolResult>
|
||||
environment?: 'local' | 'remote'
|
||||
workspaceTarget?: string
|
||||
workspaceRoot?: string
|
||||
toolResults?: AgentToolResult[]
|
||||
} = {}
|
||||
): Promise<void> => {
|
||||
@@ -329,10 +333,14 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
||||
const requestModel = selectedModel
|
||||
const selection = status.selectionSupported ? { provider: requestModel.provider } : {}
|
||||
const startedAt = Date.now()
|
||||
const workspace = agentConversationWorkspace(options.environment, options.workspaceTarget, options.workspaceRoot)
|
||||
const conversationScope = workspace ? { workspace, createdAt: startedAt } : undefined
|
||||
const conversationContext = buildAgentConversationContext(messageStateRef.current.messages, workspace, startedAt)
|
||||
const userMessage: AgentConversationMessage = {
|
||||
id: `agent-user-${Date.now()}`,
|
||||
role: 'user',
|
||||
content: request
|
||||
content: request,
|
||||
...(conversationScope ? { conversationScope } : {})
|
||||
}
|
||||
if (options.showUserMessage !== false) updateMessages((current) => [...current, userMessage])
|
||||
setPending(true)
|
||||
@@ -383,7 +391,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
||||
requestPlan: (context, finalize) => requestApi<AgentPlanResponse>('/chat/agent/plan', {
|
||||
method: 'POST',
|
||||
signal: abortController.signal,
|
||||
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), environment: options.environment, workspaceTarget: options.workspaceTarget, ...(options.executeTool ? { toolProtocol: 1 } : {}), ...(textAttachments.length ? { attachments: textAttachments } : {}), ...(imageTokens.length ? { imageTokens } : {}), ...context, ...(conversationContext ? { conversationContext } : {}) })
|
||||
})
|
||||
})
|
||||
if (!currentRequest()) return
|
||||
@@ -401,6 +409,7 @@ export function useAiAgent({ accessToken, studentEmail, courseId, weekNumber, ap
|
||||
revealStartedAt: Date.now(),
|
||||
durationMs: Math.max(0, Date.now() - startedAt),
|
||||
content: plan.summary,
|
||||
...(conversationScope ? { conversationScope } : {}),
|
||||
provider: response.provider,
|
||||
model: response.model,
|
||||
usage: response.usage,
|
||||
|
||||
Reference in New Issue
Block a user