Release v2.2.1: Autonomous Task Resumption & Engine Resilience
This commit is contained in:
+63
-11
@@ -38,8 +38,12 @@ import { detectProjectIntent, KnownProject } from './features/projectArchitectur
|
||||
import {
|
||||
readCompanyState,
|
||||
runCompanyTurn,
|
||||
resumeCompanyTurn,
|
||||
listResumableSessions,
|
||||
summarizeForChip,
|
||||
CompanyTurnEvent,
|
||||
DispatcherDeps,
|
||||
ApprovalDecision,
|
||||
COMPANY_AGENTS,
|
||||
COMPANY_AGENT_ORDER,
|
||||
ROLE_CATEGORY_LABELS,
|
||||
@@ -1680,7 +1684,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
* progress events back as `companyTurnUpdate` messages so the same bubble
|
||||
* fills in as each agent finishes.
|
||||
*/
|
||||
async _runCompanyTurn(userPrompt: string): Promise<void> {
|
||||
async _runCompanyTurn(userPrompt: string, resumeTimestamp?: string): Promise<void> {
|
||||
const cfg = getConfig();
|
||||
const ai = new AIService();
|
||||
const emit = (event: CompanyTurnEvent) => {
|
||||
@@ -1692,7 +1696,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
const abort = new AbortController();
|
||||
this._companyAbort = abort;
|
||||
try {
|
||||
await runCompanyTurn(userPrompt, {
|
||||
const deps: DispatcherDeps = {
|
||||
context: this._context,
|
||||
ai,
|
||||
defaultModel: cfg.defaultModel || 'gemma4:e2b',
|
||||
@@ -1706,21 +1710,36 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
// executor so specialist outputs like `<create_file>` actually
|
||||
// hit disk. Without this, agents would *claim* to create
|
||||
// files while nothing happened — the exact bug we just fixed.
|
||||
executeActionTags: (text) => this._agent.executeActionTagsOnText(text),
|
||||
executeActionTags: (text: string) => this._agent.executeActionTagsOnText(text),
|
||||
signal: abort.signal,
|
||||
onEvent: emit,
|
||||
// 승인 게이트 bridge — dispatcher가 호출하면 Promise를 만들어
|
||||
// resolver를 _pendingApprovals에 보관 후 await. 사용자가 카드 버튼을
|
||||
// 누르면 chatHandlers가 resolveApprovalGate(stageId, decision)을 호출
|
||||
// 하고 그 resolve가 이 await을 풀어준다.
|
||||
awaitApproval: ({ stageId }) => new Promise((resolve) => {
|
||||
if (abort.signal.aborted) {
|
||||
resolve({ kind: 'abort' });
|
||||
return;
|
||||
}
|
||||
this._pendingApprovals.set(stageId, resolve);
|
||||
}),
|
||||
});
|
||||
awaitApproval: ({ stageId }: { stageId: string; stageLabel: string }) =>
|
||||
new Promise<ApprovalDecision>((resolve) => {
|
||||
if (abort.signal.aborted) {
|
||||
resolve({ kind: 'abort' });
|
||||
return;
|
||||
}
|
||||
this._pendingApprovals.set(stageId, resolve);
|
||||
}),
|
||||
};
|
||||
// 일반 새 turn vs 재개 turn 분기. 재개 시 _resume.json에서 plan + cursor를
|
||||
// 복원해 그 다음 stage부터 dispatch가 이어진다. resumeCompanyTurn이 null을
|
||||
// 돌려주면(파일 없음·이미 완료 등) 사용자에게 알리고 종료.
|
||||
if (resumeTimestamp) {
|
||||
const result = await resumeCompanyTurn(resumeTimestamp, deps);
|
||||
if (!result) {
|
||||
this._view?.webview.postMessage({
|
||||
type: 'error',
|
||||
value: '재개 가능한 세션 정보를 찾지 못했습니다 (이미 완료되었거나 파일이 손상되었을 수 있습니다).',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await runCompanyTurn(userPrompt, deps);
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('company.runTurn: unexpected failure.', { error: e?.message ?? String(e) });
|
||||
this._view?.webview.postMessage({
|
||||
@@ -1737,6 +1756,39 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
// with the red Stop button after the round completes.
|
||||
this._view?.webview.postMessage({ type: 'streamEnd' });
|
||||
void this._sendReadyStatus();
|
||||
// turn이 끝났으면(완료든 abort든) resume 가능 세션 목록을 새로 푸시 —
|
||||
// 방금 abort된 세션이 곧장 목록에 떠야 하므로.
|
||||
void this._sendCompanyResumable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Webview에 "이어서 진행할 수 있는 세션" 목록을 push. 관리 패널이 열릴 때와 turn이
|
||||
* 끝날 때마다 호출됨. 빈 목록도 그대로 보내서 UI가 섹션을 자동으로 숨길 수 있게 함.
|
||||
*/
|
||||
async _sendCompanyResumable(): Promise<void> {
|
||||
if (!this._view) return;
|
||||
try {
|
||||
const items = listResumableSessions(this._context).map((s) => ({
|
||||
timestamp: s.timestamp,
|
||||
userPrompt: s.userPrompt.slice(0, 200),
|
||||
pipelineId: s.pipelineId,
|
||||
pipelineName: s.pipelineId
|
||||
? (readCompanyState(this._context).pipelines?.[s.pipelineId]?.name ?? s.pipelineId)
|
||||
: null,
|
||||
completedCount: s.agentOutputs.length,
|
||||
totalCount: s.plan.tasks.length,
|
||||
status: s.status,
|
||||
abortReason: s.abortReason ?? '',
|
||||
lastUpdatedAt: s.lastUpdatedAt,
|
||||
startedAt: s.startedAt,
|
||||
}));
|
||||
this._view.webview.postMessage({
|
||||
type: 'companyResumable',
|
||||
value: { items },
|
||||
});
|
||||
} catch (e: any) {
|
||||
logError('company._sendCompanyResumable failed.', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user