Skip to content

Commit a1a5a01

Browse files
committed
我打算直接做成一个完整的agent
1 parent db02049 commit a1a5a01

5 files changed

Lines changed: 183 additions & 9 deletions

File tree

electron/agent/query/queryEngine.ts

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,21 @@ export class AgentQueryEngine {
6868
goal: string,
6969
options: AgentTaskRunOptions,
7070
): Promise<boolean> {
71+
const activeRunMode = session.activeTaskRun?.mode;
7172
const siteFollowUpGoal = looksLikeSiteFollowUpGoal(goal);
7273
const continuingRun =
7374
options.resumeRequested &&
7475
Boolean(session.activeTaskRun) &&
7576
!['completed', 'failed'].includes(session.activeTaskRun?.status || '');
7677

78+
if (continuingRun && activeRunMode && activeRunMode !== 'project') {
79+
return this.runGenericTask(session, goal, {
80+
continuingRun: true,
81+
sourceLabel: session.activeTaskRun?.source?.label,
82+
mode: activeRunMode,
83+
});
84+
}
85+
7786
const sourceLabel = continuingRun
7887
? session.activeTaskRun?.source?.label || await this.store.resolveDeploySource(session, goal)
7988
: await this.store.resolveDeploySource(session, goal);
@@ -94,23 +103,69 @@ export class AgentQueryEngine {
94103
return this.runGenericTask(session, goal);
95104
}
96105

97-
async runGenericTask(session: AgentThreadSession, goal: string): Promise<boolean> {
106+
async runGenericTask(
107+
session: AgentThreadSession,
108+
goal: string,
109+
options?: {
110+
continuingRun?: boolean;
111+
sourceLabel?: string;
112+
mode?: 'generic' | 'site-followup';
113+
},
114+
): Promise<boolean> {
98115
session.planState.global_goal = goal;
99116
session.planState.scratchpad = appendScratchpad(session.planState.scratchpad, `Goal: ${goal}`);
100117
this.events.emitPlanUpdate(session, 'generating');
101118

119+
const continuingRun = Boolean(options?.continuingRun && session.activeTaskRun);
120+
if (!continuingRun) {
121+
const run = this.store.createGenericTaskRun(goal, {
122+
mode: options?.mode || 'generic',
123+
sourceLabel: options?.sourceLabel,
124+
currentAction: options?.mode === 'site-followup'
125+
? 'Inspecting the current site, nginx, and certificate state'
126+
: 'Understanding the goal and deciding the next action',
127+
nextAction: 'Inspect the current state and continue execution',
128+
});
129+
this.store.attachTaskRun(session, run);
130+
session.recentHttpProbes = [];
131+
session.lastToolFailure = undefined;
132+
} else if (session.activeTaskRun) {
133+
this.store.upsertTaskRun(session, {
134+
status: 'running',
135+
phase: 'act',
136+
currentAction: options?.mode === 'site-followup'
137+
? 'Resuming the site follow-up task'
138+
: 'Resuming the current task',
139+
}, {
140+
phase: 'act',
141+
nextAction: 'Continue from the preserved task state',
142+
});
143+
}
144+
102145
let completed = false;
103146
try {
104147
while (!session.aborted && session.turnCounter < MAX_GENERIC_TURNS) {
105148
session.turnCounter += 1;
106149
await this.compactService.maybeCompact(session);
150+
if (session.activeTaskRun) {
151+
this.store.upsertTaskRun(session, {
152+
status: 'running',
153+
phase: 'act',
154+
currentAction: options?.mode === 'site-followup'
155+
? 'Inspecting and updating the current site configuration'
156+
: 'Thinking, inspecting facts, and deciding the next action',
157+
}, {
158+
phase: 'act',
159+
nextAction: 'Continue the current task loop',
160+
});
161+
}
107162
this.events.emitPlanUpdate(session, 'executing');
108163

109164
const response = await this.callLLMWithRetries(session);
110165
this.store.updateContextWindow(session, response.usage);
166+
const text = response.content?.trim() || '';
111167

112-
if (response.content?.trim()) {
113-
const text = response.content.trim();
168+
if (text) {
114169
this.events.emitAssistantMessage(session, {
115170
id: `assistant-${Date.now()}`,
116171
role: 'assistant',
@@ -125,6 +180,18 @@ export class AgentQueryEngine {
125180
}
126181

127182
if (!response.toolCalls?.length) {
183+
if (session.activeTaskRun) {
184+
const verifiedUrl = this.store.detectVerifiedUrl(session, text);
185+
this.store.upsertTaskRun(session, {
186+
status: 'completed',
187+
phase: 'complete',
188+
finalUrl: verifiedUrl,
189+
currentAction: 'The task finished and produced a final answer.',
190+
}, {
191+
phase: 'complete',
192+
nextAction: undefined,
193+
});
194+
}
128195
completed = true;
129196
return true;
130197
}
@@ -139,6 +206,16 @@ export class AgentQueryEngine {
139206
}
140207

141208
const limitMessage = 'The current task reached the autonomous turn budget. Context is preserved, and you can ask me to continue.';
209+
if (session.activeTaskRun) {
210+
this.store.upsertTaskRun(session, {
211+
status: 'paused',
212+
phase: 'paused',
213+
currentAction: limitMessage,
214+
}, {
215+
phase: 'paused',
216+
nextAction: 'Send continue to resume the same task',
217+
});
218+
}
142219
this.store.historyPush(session, { role: 'assistant', content: limitMessage });
143220
this.events.emitAssistantMessage(session, {
144221
id: `limit-${Date.now()}`,
@@ -148,8 +225,49 @@ export class AgentQueryEngine {
148225
isError: true,
149226
});
150227
return true;
228+
} catch (error: any) {
229+
const failureClass = this.store.classifyAutonomousFailure(undefined, error?.message || String(error));
230+
const failure: TaskRunFailure = {
231+
attempt: Math.max((session.activeTaskRun?.attemptCount || 0) + 1, 1),
232+
routeId: session.activeTaskRun?.activeHypothesisId,
233+
failureClass,
234+
message: error?.message || String(error),
235+
timestamp: now(),
236+
};
237+
if (session.activeTaskRun) {
238+
this.store.upsertTaskRun(session, {
239+
status: failureClass === 'llm_overloaded' ? 'retryable_paused' : 'failed',
240+
phase: failureClass === 'llm_overloaded' ? 'paused' : 'failed',
241+
attemptCount: failure.attempt,
242+
failureHistory: [...(session.activeTaskRun.failureHistory || []), failure].slice(-20),
243+
currentAction: this.store.failureText(failure, true),
244+
}, {
245+
phase: failureClass === 'llm_overloaded' ? 'paused' : 'failed',
246+
attemptCount: failure.attempt,
247+
nextAction: failureClass === 'llm_overloaded' ? 'Send continue to resume the same task' : undefined,
248+
});
249+
}
250+
const failureText = this.store.failureText(failure, true);
251+
this.store.historyPush(session, { role: 'assistant', content: failureText });
252+
this.events.emitAssistantMessage(session, {
253+
id: `generic-task-error-${Date.now()}`,
254+
role: 'assistant',
255+
content: failureText,
256+
timestamp: now(),
257+
isError: true,
258+
});
259+
return true;
151260
} finally {
152-
this.events.emitPlanUpdate(session, session.aborted ? 'stopped' : completed ? 'done' : 'stopped');
261+
this.events.emitPlanUpdate(
262+
session,
263+
session.activeTaskRun
264+
? phaseToPlanStatus(session.activeTaskRun)
265+
: session.aborted
266+
? 'stopped'
267+
: completed
268+
? 'done'
269+
: 'stopped',
270+
);
153271
}
154272
}
155273

@@ -174,7 +292,10 @@ export class AgentQueryEngine {
174292
: 'I will treat this as an existing-site operation and inspect the current server, nginx, and certificate state before applying domain and HTTPS changes.',
175293
timestamp: now(),
176294
});
177-
return this.runGenericTask(session, goal);
295+
return this.runGenericTask(session, goal, {
296+
mode: 'site-followup',
297+
sourceLabel: inheritedSource,
298+
});
178299
}
179300

180301
async executeRoute(session: AgentThreadSession, route: RouteHypothesis): Promise<RouteExecutionResult> {

electron/agent/runtime/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ const WINDOWS_LOCAL_PROJECT_PATH_RE = /(?:[A-Za-z]:\\|\\\\)[^\r\n"'`<>|,,。
88
const POSIX_LOCAL_PROJECT_PATH_RE = /\/(?:Users|home|opt|srv|var|tmp)[^\s\r\n"'`<>|,]*/g;
99

1010
export const CONTINUE_INTENT_RE = /^(?:continue|resume|retry|go on|keep going||||||||)\s*[,!?:;]*$/i;
11-
export const STATUS_QUERY_RE = /^(?:status|what are you doing|what's the current status|what is the current status||||||)\s*[?!]*$/i;
11+
export const STATUS_QUERY_RE = /^(?:status|what are you doing|what's the current status|what is the current status||||||)\s*[?!]*$/i;
1212
export const OPTION_SELECTION_RE = /^(?:[ab]|[12]|option\s*[ab12]|\s*[ab]|\s*[ab12])$/i;
1313
export const LOCAL_PROJECT_PATH_RE = process.platform === 'win32'
1414
? WINDOWS_LOCAL_PROJECT_PATH_RE
1515
: POSIX_LOCAL_PROJECT_PATH_RE;
1616
export const GITHUB_PROJECT_URL_RE = /https?:\/\/github\.com\/[^\s"'`<>]+/ig;
17-
export const MAX_GENERIC_TURNS = 32;
17+
export const MAX_GENERIC_TURNS = 48;
1818
export const MAX_AUTONOMOUS_REPAIRS = 5;
1919

2020
const TOOL_LABELS: Record<string, string> = {

electron/agent/state/sessionStore.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ export class AgentSessionStore {
121121
activeTaskRun: restored?.activeTaskRun
122122
? {
123123
...restored.activeTaskRun,
124+
mode: restored.activeTaskRun.mode
125+
|| (restored.activeTaskRun.source ? 'project' : 'generic'),
124126
childRuns: restored.activeTaskRun.childRuns || [],
125127
taskTodos: restored.activeTaskRun.taskTodos || restored?.taskTodos || this.createDefaultTodos(),
126128
}
@@ -293,6 +295,7 @@ export class AgentSessionStore {
293295
return {
294296
id: buildTaskRunId(),
295297
goal,
298+
mode: 'project',
296299
status: 'running',
297300
phase: 'understand',
298301
source: {
@@ -318,6 +321,47 @@ export class AgentSessionStore {
318321
};
319322
}
320323

324+
createGenericTaskRun(
325+
goal: string,
326+
options?: {
327+
mode?: 'generic' | 'site-followup';
328+
sourceLabel?: string;
329+
currentAction?: string;
330+
nextAction?: string;
331+
},
332+
): TaskRunSummary {
333+
const createdAt = now();
334+
const taskTodos = this.createDefaultTodos();
335+
return {
336+
id: buildTaskRunId(),
337+
goal,
338+
mode: options?.mode || 'generic',
339+
status: 'running',
340+
phase: 'act',
341+
source: options?.sourceLabel
342+
? {
343+
type: /^https?:\/\/github\.com\//i.test(options.sourceLabel) ? 'github' : 'local',
344+
label: options.sourceLabel,
345+
}
346+
: undefined,
347+
hypotheses: [],
348+
attemptCount: 0,
349+
failureHistory: [],
350+
checkpoint: {
351+
phase: 'act',
352+
completedActions: [],
353+
knownFacts: [],
354+
attemptCount: 0,
355+
nextAction: options?.nextAction,
356+
},
357+
taskTodos,
358+
childRuns: [],
359+
currentAction: options?.currentAction || 'Working on the current task',
360+
createdAt,
361+
updatedAt: createdAt,
362+
};
363+
}
364+
321365
attachTaskRun(session: AgentThreadSession, taskRun: TaskRunSummary) {
322366
session.activeTaskRun = taskRun;
323367
session.activeRunId = taskRun.id;

src/components/AIChatPanel.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const LOCAL_PROJECT_PATH_RE = (() => {
5252
})();
5353
const CONTINUE_INTENT_RE = /^(||||||||continue|resume|retry)\s*[.!]?$/i;
5454
const OPTION_SELECTION_RE = /^(?:[ab]|[12]|option\s*[ab12]|\s*[ab]|\s*[ab12])$/i;
55+
const STATUS_QUERY_RE = /^(?:status|what are you doing|what's the current status|what is the current status||||||)\s*[?!]*$/i;
5556

5657
function extractDeployProjectPath(input: string): string | null {
5758
const matches = input.match(LOCAL_PROJECT_PATH_RE);
@@ -732,6 +733,7 @@ export function AIChatPanel({
732733
if (!input.trim() || isLoading) return;
733734
const trimmedInput = input.trim();
734735
const isContinueMessage = CONTINUE_INTENT_RE.test(trimmedInput) || OPTION_SELECTION_RE.test(trimmedInput);
736+
const isStatusMessage = STATUS_QUERY_RE.test(trimmedInput);
735737

736738
if (!aiService.isConfigured()) {
737739
const errorMsg: AgentMessage = {
@@ -756,10 +758,14 @@ export function AIChatPanel({
756758
setInput('');
757759

758760
// Reset plan state for a new goal, but preserve it when the user is continuing the same run.
761+
const hasResumableRun = Boolean(activeTaskRun && !['completed', 'failed'].includes(activeTaskRun.status));
759762
const isResuming = planMode
763+
&& hasResumableRun
760764
&& (
761-
((planStatus === 'paused' || planStatus === 'waiting_approval') && planStateRef.current !== null)
762-
|| (planStatus === 'stopped' && isContinueMessage && (planStateRef.current !== null || messages.length > 0))
765+
isContinueMessage
766+
|| isStatusMessage
767+
|| ((planStatus === 'paused' || planStatus === 'waiting_approval') && planStateRef.current !== null)
768+
|| (planStatus === 'stopped' && (planStateRef.current !== null || messages.length > 0))
763769
);
764770
if (!isResuming) {
765771
setPlanState(null);

src/shared/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ export type TaskRunPhase =
118118
| 'failed'
119119
| 'paused';
120120

121+
export type TaskRunMode = 'project' | 'generic' | 'site-followup';
122+
121123
export interface RouteHypothesis {
122124
id: string;
123125
kind: RouteHypothesisKind;
@@ -166,6 +168,7 @@ export interface RunCheckpoint {
166168
export interface TaskRunSummary {
167169
id: string;
168170
goal: string;
171+
mode: TaskRunMode;
169172
status: TaskRunStatus;
170173
phase: TaskRunPhase;
171174
source?: {

0 commit comments

Comments
 (0)