Skip to content

Commit f222d48

Browse files
committed
refactor: extract frontagent task execution setup
1 parent 3a24a50 commit f222d48

4 files changed

Lines changed: 422 additions & 39 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# FrontAgent Task Execution Setup Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Extract focused task execution setup from `FrontAgent.execute` while preserving public APIs, emitted event sequences, planner/executor contracts, SDD behavior, and memory persistence semantics.
6+
7+
**Architecture:** Add an internal helper that prepares the shared task inputs, setup status emissions, memory preload, SDD system prompts, and project planning preparation currently owned inline by `FrontAgent.execute`. `FrontAgent.execute` remains the high-level coordinator for task creation, initial task event emission, planning, execution, final output, memory persistence, and cleanup.
8+
9+
**Tech Stack:** TypeScript, Vitest, pnpm, GitNexus CLI.
10+
11+
---
12+
13+
### Task 1: Add Focused Setup Helper
14+
15+
**Files:**
16+
- Create: `packages/core/src/agent/task-execution-setup.ts`
17+
- Modify: `packages/core/src/agent/agent.ts`
18+
- Test: `packages/core/src/agent/task-execution-setup.test.ts`
19+
20+
**GitNexus Blast Radius:**
21+
- `execute` in `packages/core/src/agent/agent.ts`: upstream direct callers 0, affected processes 0, affected modules 0, risk LOW.
22+
- `planOnly` in `packages/core/src/agent/agent.ts`: upstream direct callers 0, affected processes 0, affected modules 0, risk LOW.
23+
- `context execute` shows dependency on `preloadMemory`, `prepareProjectPlanningContext`, `emit`, `emitStatus`, and abort/status helpers; the helper will preserve those calls and order for the setup phase.
24+
25+
**Helper Scope:**
26+
- Shared inputs: pre-created `AgentTask`, original task description, matched skill names, skill prompt context, task id, task context metadata, and context manager task context.
27+
- Status phase: setup-only status emissions after `task_started`/initialization, covering memory-load and planning preparation labels.
28+
- Memory phase: `memoryStore.resetSession()` and `preloadMemory(...)` only; final `persistMemory(...)` remains in `FrontAgent.execute` `finally`.
29+
- SDD phase: constitution prompt and generated SDD prompt insertion into the context manager.
30+
- Planning preparation phase: `prepareProjectPlanningContext(...)` invocation and `rag_retrieved` event emission.
31+
- Non-scope: planner retry/fallback handling, executor step execution, final answer generation, memory persistence, cleanup, and tool registration methods.
32+
33+
- [x] **Step 1: Write failing helper tests**
34+
35+
Create `packages/core/src/agent/task-execution-setup.test.ts` with tests that call the helper directly using stubbed dependencies:
36+
37+
```ts
38+
expect(events.map((event) => event.type)).toEqual([
39+
'status_update',
40+
'status_update',
41+
'status_update',
42+
'status_update',
43+
'rag_retrieved',
44+
]);
45+
expect(preloadMemory).toHaveBeenCalledOnce();
46+
expect(context.collectedContext.metadata.originalTaskDescription).toBe('Use @skill-a for this');
47+
expect(context.collectedContext.skillContext).toBe('skill prompt');
48+
expect(context.collectedContext.matchedSkillNames).toEqual(['skill-a']);
49+
```
50+
51+
- [x] **Step 2: Run test to verify it fails**
52+
53+
Run:
54+
55+
```bash
56+
pnpm --filter @frontagent/core exec vitest run src/agent/task-execution-setup.test.ts
57+
```
58+
59+
Expected: FAIL because `./task-execution-setup.js` does not exist yet.
60+
61+
- [x] **Step 3: Implement minimal helper**
62+
63+
Create `packages/core/src/agent/task-execution-setup.ts` exporting `prepareTaskExecutionSetup(...)`. It should accept dependencies from `FrontAgent.execute`, perform only the setup/helper scope above, and return `{ task, context, planningPreparation, skillContext, matchedSkillNames }`.
64+
65+
- [x] **Step 4: Replace inline setup in `FrontAgent.execute`**
66+
67+
Modify `packages/core/src/agent/agent.ts` so `execute` delegates setup through `prepareTaskExecutionSetup(...)`, then keeps the existing planning, retry/fallback, execution, final answer, failure, persistence, and cleanup orchestration unchanged.
68+
69+
- [x] **Step 5: Verify focused behavior**
70+
71+
Run:
72+
73+
```bash
74+
pnpm --filter @frontagent/core exec vitest run src/agent/task-execution-setup.test.ts src/agent/agent.test.ts src/agent/execution-callbacks.test.ts
75+
```
76+
77+
Expected: PASS.
78+
79+
- [x] **Step 6: Verify contracts before PR**
80+
81+
Run:
82+
83+
```bash
84+
pnpm typecheck
85+
npx gitnexus detect-changes --scope all --repo /Users/ceilf6/.config/superpowers/worktrees/FrontAgent-app/improve-frontagent-task-execution-setup
86+
pnpm quality:precommit
87+
```
88+
89+
Expected: PASS, with GitNexus showing only the task execution setup helper extraction, focused test, plan file, and expected `FrontAgent.execute` impact.

‎packages/core/src/agent/agent.ts‎

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { createExecutionCallbacks } from './execution-callbacks.js';
4343
import { FactsUpdateFlusher } from './facts-update-flush.js';
4444
import { persistMemory, preloadMemory } from './memory-lifecycle.js';
4545
import { prepareProjectPlanningContext } from './project-prescan-preparation.js';
46+
import { prepareTaskExecutionSetup } from './task-execution-setup.js';
4647

4748
/**
4849
* FrontAgent 主类
@@ -570,49 +571,31 @@ export class FrontAgent {
570571
this.throwIfAborted(options?.signal);
571572
this.currentTaskId = task.id;
572573

573-
const context = this.contextManager.createContext(task, this.sddConfig);
574-
context.collectedContext.skillContext = skillContext;
575-
context.collectedContext.matchedSkillNames = matchedSkillNames;
576-
context.collectedContext.metadata.originalTaskDescription = taskDescription;
577-
578-
this.emitStatus('加载跨会话记忆', '加载跨会话记忆');
579-
this.memoryStore.resetSession();
580-
preloadMemory(this.memoryDeps, task.id, context);
581-
582-
if (this.promptGenerator) {
583-
const constitutionPrompt = this.workflowIntegration?.getConstitutionPrompt();
584-
if (constitutionPrompt) {
585-
this.contextManager.addMessage(task.id, { role: 'system', content: constitutionPrompt });
586-
}
587-
const sddPrompt = this.promptGenerator.generate();
588-
this.contextManager.addMessage(task.id, {
589-
role: 'system',
590-
content: sddPrompt,
591-
});
592-
}
593-
594-
const planningPreparation = await prepareProjectPlanningContext({
574+
const setup = await prepareTaskExecutionSetup({
575+
task,
576+
originalTaskDescription: taskDescription,
577+
skillContext,
578+
matchedSkillNames,
595579
deps: {
596-
executor: this.executor,
597-
ragDeps: this.ragDeps,
580+
config: this.config,
581+
sddConfig: this.sddConfig,
582+
contextManager: this.contextManager,
583+
memoryStore: this.memoryStore,
584+
memoryDeps: this.memoryDeps,
585+
promptGenerator: this.promptGenerator,
586+
workflowIntegration: this.workflowIntegration,
587+
planningDeps: {
588+
executor: this.executor,
589+
ragDeps: this.ragDeps,
590+
emitStatus: this.emitStatus.bind(this),
591+
debugLog: this.debugLog.bind(this),
592+
debugWarn: this.debugWarn.bind(this),
593+
},
594+
emit: this.emit.bind(this),
598595
emitStatus: this.emitStatus.bind(this),
599-
debugLog: this.debugLog.bind(this),
600-
debugWarn: this.debugWarn.bind(this),
601596
},
602-
taskId: task.id,
603-
taskDescription: task.description,
604-
projectRoot: this.config.projectRoot,
605-
ragEnabled: this.config.rag?.enabled !== false,
606-
preScanFailureLabel: '[Agent] Failed to pre-scan project structure:',
607-
logProjectStructure: true,
608597
});
609-
610-
if (this.config.rag?.enabled !== false) {
611-
this.emit({
612-
type: 'rag_retrieved',
613-
...planningPreparation.ragEvent,
614-
});
615-
}
598+
const { context, planningPreparation } = setup;
616599

617600
this.emit({ type: 'planning_started' });
618601
this.emitStatus('生成执行计划', 'LLM 规划');
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import type { AgentTask } from '@frontagent/shared';
2+
import { describe, expect, it, vi } from 'vitest';
3+
import type { AgentEvent } from '../types.js';
4+
import { prepareTaskExecutionSetup } from './task-execution-setup.js';
5+
6+
function makeTask(description = 'Use skill text'): AgentTask {
7+
return {
8+
id: 'task-1',
9+
type: 'code',
10+
description,
11+
context: {
12+
workingDirectory: '/repo',
13+
relevantFiles: ['src/app.ts'],
14+
browserUrl: 'http://localhost:5173',
15+
},
16+
};
17+
}
18+
19+
function makeContext() {
20+
return {
21+
collectedContext: {
22+
files: new Map<string, string>(),
23+
pageStructure: undefined,
24+
ragResults: undefined,
25+
skillContext: undefined,
26+
matchedSkillNames: [] as string[],
27+
memoryContext: undefined,
28+
metadata: {} as Record<string, unknown>,
29+
},
30+
};
31+
}
32+
33+
function makeSetupDeps({ ragEnabled = true }: { ragEnabled?: boolean } = {}) {
34+
const events: AgentEvent[] = [];
35+
const statusUpdates: Array<{ label: string; operation?: string; detail?: string }> = [];
36+
const context = makeContext();
37+
const createdTasks: AgentTask[] = [];
38+
const preloadMemory = vi.fn();
39+
const prepareProjectPlanningContext = vi.fn(
40+
async ({ deps }: { deps: { emitStatus: typeof emitStatus } }) => {
41+
deps.emitStatus('扫描项目结构', 'list_directory 扫描项目结构');
42+
deps.emitStatus('检测开发服务器端口', '检测开发服务器端口');
43+
deps.emitStatus('检索知识库', 'RAG 检索');
44+
return {
45+
projectStructure: 'project files',
46+
devServerPort: 5173,
47+
ragResults: ['rag result'],
48+
ragEvent: {
49+
searchMode: 'keyword_only' as const,
50+
matches: [
51+
{
52+
type: 'doc',
53+
title: 'Guide',
54+
sourceUrl: 'https://example.test',
55+
snippet: 'Use it.',
56+
},
57+
],
58+
},
59+
};
60+
},
61+
);
62+
const emit = vi.fn((event: AgentEvent) => {
63+
events.push(event);
64+
});
65+
const emitStatus = vi.fn((label: string, operation = label, detail?: string) => {
66+
statusUpdates.push({ label, operation, detail });
67+
emit({ type: 'status_update', label, operation, detail });
68+
});
69+
const addMessage = vi.fn();
70+
const promptGenerator = { generate: vi.fn(() => 'sdd prompt') };
71+
const workflowIntegration = { getConstitutionPrompt: vi.fn(() => 'constitution prompt') };
72+
73+
return {
74+
events,
75+
statusUpdates,
76+
context,
77+
createdTasks,
78+
preloadMemory,
79+
prepareProjectPlanningContext,
80+
addMessage,
81+
promptGenerator,
82+
workflowIntegration,
83+
deps: {
84+
config: {
85+
projectRoot: '/repo',
86+
llm: { provider: 'openai' as const, model: 'gpt-4', apiKey: 'test' },
87+
rag: ragEnabled
88+
? { repoUrl: 'https://example.test/rag.git' }
89+
: { enabled: false, repoUrl: 'https://example.test/rag.git' },
90+
},
91+
contextManager: {
92+
createContext: vi.fn((task: AgentTask) => {
93+
createdTasks.push(task);
94+
return context;
95+
}),
96+
addMessage,
97+
},
98+
memoryStore: { resetSession: vi.fn() },
99+
memoryDeps: {},
100+
promptGenerator,
101+
workflowIntegration,
102+
planningDeps: {
103+
executor: { callTool: vi.fn() },
104+
ragDeps: {},
105+
emitStatus,
106+
debugLog: vi.fn(),
107+
debugWarn: vi.fn(),
108+
},
109+
preloadMemory,
110+
prepareProjectPlanningContext,
111+
emit,
112+
emitStatus,
113+
debugLog: vi.fn(),
114+
},
115+
};
116+
}
117+
118+
describe('prepareTaskExecutionSetup', () => {
119+
it('prepares task inputs, status, memory, SDD prompts, and planning context in execute order', async () => {
120+
const setup = makeSetupDeps();
121+
const task = makeTask();
122+
123+
const result = await prepareTaskExecutionSetup({
124+
task,
125+
originalTaskDescription: 'Use @skill-a for this',
126+
skillContext: 'skill prompt',
127+
matchedSkillNames: ['skill-a'],
128+
deps: setup.deps,
129+
});
130+
131+
expect(result.task).toBe(task);
132+
expect(result.context).toBe(setup.context);
133+
expect(result.skillContext).toBe('skill prompt');
134+
expect(result.matchedSkillNames).toEqual(['skill-a']);
135+
expect(result.planningPreparation).toMatchObject({
136+
projectStructure: 'project files',
137+
devServerPort: 5173,
138+
ragResults: ['rag result'],
139+
});
140+
expect(setup.context.collectedContext.skillContext).toBe('skill prompt');
141+
expect(setup.context.collectedContext.matchedSkillNames).toEqual(['skill-a']);
142+
expect(setup.context.collectedContext.metadata.originalTaskDescription).toBe(
143+
'Use @skill-a for this',
144+
);
145+
expect(setup.deps.memoryStore.resetSession).toHaveBeenCalledOnce();
146+
expect(setup.preloadMemory).toHaveBeenCalledWith(
147+
setup.deps.memoryDeps,
148+
result.task.id,
149+
setup.context,
150+
);
151+
expect(setup.addMessage).toHaveBeenCalledWith(result.task.id, {
152+
role: 'system',
153+
content: 'constitution prompt',
154+
});
155+
expect(setup.addMessage).toHaveBeenCalledWith(result.task.id, {
156+
role: 'system',
157+
content: 'sdd prompt',
158+
});
159+
expect(setup.events.map((event) => event.type)).toEqual([
160+
'status_update',
161+
'status_update',
162+
'status_update',
163+
'status_update',
164+
'rag_retrieved',
165+
]);
166+
expect(setup.statusUpdates.map((status) => [status.label, status.operation])).toEqual([
167+
['加载跨会话记忆', '加载跨会话记忆'],
168+
['扫描项目结构', 'list_directory 扫描项目结构'],
169+
['检测开发服务器端口', '检测开发服务器端口'],
170+
['检索知识库', 'RAG 检索'],
171+
]);
172+
});
173+
174+
it('keeps sanitized empty task descriptions from replacing the original and suppresses rag events when disabled', async () => {
175+
const setup = makeSetupDeps({ ragEnabled: false });
176+
const task = makeTask('Original task');
177+
178+
const result = await prepareTaskExecutionSetup({
179+
task,
180+
originalTaskDescription: 'Original task',
181+
skillContext: 'skill prompt',
182+
matchedSkillNames: ['skill-a'],
183+
deps: setup.deps,
184+
});
185+
186+
expect(result.task.description).toBe('Original task');
187+
expect(setup.prepareProjectPlanningContext).toHaveBeenCalledWith(
188+
expect.objectContaining({
189+
taskId: result.task.id,
190+
taskDescription: 'Original task',
191+
projectRoot: '/repo',
192+
ragEnabled: false,
193+
preScanFailureLabel: '[Agent] Failed to pre-scan project structure:',
194+
logProjectStructure: true,
195+
}),
196+
);
197+
expect(setup.events.some((event) => event.type === 'rag_retrieved')).toBe(false);
198+
});
199+
});

0 commit comments

Comments
 (0)