TAKTのメタ情報をエージェントに引き渡す。またTAKTの前回セッションの情報をscoreフェーズに追加 resolved #89
This commit is contained in:
parent
e932d647c6
commit
792f61df55
@ -50,7 +50,11 @@ vi.mock('../features/tasks/execute/selectAndExecute.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/loaders/pieceResolver.js', () => ({
|
vi.mock('../infra/config/loaders/pieceResolver.js', () => ({
|
||||||
getPieceDescription: vi.fn(() => ({ name: 'default', description: '' })),
|
getPieceDescription: vi.fn(() => ({
|
||||||
|
name: 'default',
|
||||||
|
description: '',
|
||||||
|
pieceStructure: '1. implement\n2. review'
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/issue.js', () => ({
|
vi.mock('../infra/github/issue.js', () => ({
|
||||||
@ -92,7 +96,7 @@ function setupFullFlowMocks(overrides?: {
|
|||||||
const slug = overrides?.slug ?? 'add-auth';
|
const slug = overrides?.slug ?? 'add-auth';
|
||||||
|
|
||||||
mockDeterminePiece.mockResolvedValue('default');
|
mockDeterminePiece.mockResolvedValue('default');
|
||||||
mockGetPieceDescription.mockReturnValue({ name: 'default', description: '' });
|
mockGetPieceDescription.mockReturnValue({ name: 'default', description: '', pieceStructure: '' });
|
||||||
mockInteractiveMode.mockResolvedValue({ confirmed: true, task });
|
mockInteractiveMode.mockResolvedValue({ confirmed: true, task });
|
||||||
mockSummarizeTaskName.mockResolvedValue(slug);
|
mockSummarizeTaskName.mockResolvedValue(slug);
|
||||||
mockConfirm.mockResolvedValue(false);
|
mockConfirm.mockResolvedValue(false);
|
||||||
@ -104,7 +108,7 @@ beforeEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
testDir = fs.mkdtempSync(path.join(tmpdir(), 'takt-test-'));
|
testDir = fs.mkdtempSync(path.join(tmpdir(), 'takt-test-'));
|
||||||
mockDeterminePiece.mockResolvedValue('default');
|
mockDeterminePiece.mockResolvedValue('default');
|
||||||
mockGetPieceDescription.mockReturnValue({ name: 'default', description: '' });
|
mockGetPieceDescription.mockReturnValue({ name: 'default', description: '', pieceStructure: '' });
|
||||||
mockConfirm.mockResolvedValue(false);
|
mockConfirm.mockResolvedValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -225,7 +229,7 @@ describe('addTask', () => {
|
|||||||
// Given: determinePiece returns a non-default piece
|
// Given: determinePiece returns a non-default piece
|
||||||
setupFullFlowMocks({ slug: 'with-piece' });
|
setupFullFlowMocks({ slug: 'with-piece' });
|
||||||
mockDeterminePiece.mockResolvedValue('review');
|
mockDeterminePiece.mockResolvedValue('review');
|
||||||
mockGetPieceDescription.mockReturnValue({ name: 'review', description: 'Code review piece' });
|
mockGetPieceDescription.mockReturnValue({ name: 'review', description: 'Code review piece', pieceStructure: '' });
|
||||||
mockConfirm.mockResolvedValue(false);
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|||||||
@ -46,6 +46,8 @@ function createMinimalContext(overrides: Partial<InstructionContext> = {}): Inst
|
|||||||
cwd: '/project',
|
cwd: '/project',
|
||||||
projectCwd: '/project',
|
projectCwd: '/project',
|
||||||
userInputs: [],
|
userInputs: [],
|
||||||
|
pieceName: 'test-piece',
|
||||||
|
pieceDescription: 'Test piece description',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -299,6 +301,62 @@ describe('instruction-builder', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('auto-injected Piece Context section', () => {
|
describe('auto-injected Piece Context section', () => {
|
||||||
|
it('should include piece name when provided', () => {
|
||||||
|
const step = createMinimalStep('Do work');
|
||||||
|
const context = createMinimalContext({
|
||||||
|
pieceName: 'my-piece',
|
||||||
|
language: 'en',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = buildInstruction(step, context);
|
||||||
|
|
||||||
|
expect(result).toContain('## Piece Context');
|
||||||
|
expect(result).toContain('- Piece: my-piece');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include piece description when provided', () => {
|
||||||
|
const step = createMinimalStep('Do work');
|
||||||
|
const context = createMinimalContext({
|
||||||
|
pieceName: 'my-piece',
|
||||||
|
pieceDescription: 'A test piece for validation',
|
||||||
|
language: 'en',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = buildInstruction(step, context);
|
||||||
|
|
||||||
|
expect(result).toContain('## Piece Context');
|
||||||
|
expect(result).toContain('- Piece: my-piece');
|
||||||
|
expect(result).toContain('- Description: A test piece for validation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show description when not provided', () => {
|
||||||
|
const step = createMinimalStep('Do work');
|
||||||
|
const context = createMinimalContext({
|
||||||
|
pieceName: 'my-piece',
|
||||||
|
pieceDescription: undefined,
|
||||||
|
language: 'en',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = buildInstruction(step, context);
|
||||||
|
|
||||||
|
expect(result).toContain('- Piece: my-piece');
|
||||||
|
expect(result).not.toContain('- Description:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render piece context in Japanese', () => {
|
||||||
|
const step = createMinimalStep('Do work');
|
||||||
|
const context = createMinimalContext({
|
||||||
|
pieceName: 'coding',
|
||||||
|
pieceDescription: 'コーディングピース',
|
||||||
|
language: 'ja',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = buildInstruction(step, context);
|
||||||
|
|
||||||
|
expect(result).toContain('- ピース: coding');
|
||||||
|
expect(result).toContain('- 説明: コーディングピース');
|
||||||
|
});
|
||||||
|
|
||||||
it('should include iteration, step iteration, and step name', () => {
|
it('should include iteration, step iteration, and step name', () => {
|
||||||
const step = createMinimalStep('Do work');
|
const step = createMinimalStep('Do work');
|
||||||
step.name = 'implement';
|
step.name = 'implement';
|
||||||
|
|||||||
@ -31,6 +31,8 @@ vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
|||||||
loadAgentSessions: vi.fn(() => ({})),
|
loadAgentSessions: vi.fn(() => ({})),
|
||||||
updateAgentSession: vi.fn(),
|
updateAgentSession: vi.fn(),
|
||||||
getProjectConfigDir: vi.fn(() => '/tmp'),
|
getProjectConfigDir: vi.fn(() => '/tmp'),
|
||||||
|
loadSessionState: vi.fn(() => null),
|
||||||
|
clearSessionState: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -279,4 +281,59 @@ describe('interactiveMode', () => {
|
|||||||
expect(result.confirmed).toBe(true);
|
expect(result.confirmed).toBe(true);
|
||||||
expect(result.task).toBe('Fix login page with clarified scope.');
|
expect(result.task).toBe('Fix login page with clarified scope.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('/play command', () => {
|
||||||
|
it('should return confirmed=true with task on /play command', async () => {
|
||||||
|
// Given
|
||||||
|
setupInputSequence(['/play implement login feature']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.confirmed).toBe(true);
|
||||||
|
expect(result.task).toBe('implement login feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error when /play has no task content', async () => {
|
||||||
|
// Given: /play without task, then /cancel to exit
|
||||||
|
setupInputSequence(['/play', '/cancel']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then: should not confirm (fell through to /cancel)
|
||||||
|
expect(result.confirmed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle /play with leading/trailing spaces', async () => {
|
||||||
|
// Given
|
||||||
|
setupInputSequence(['/play test task ']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.confirmed).toBe(true);
|
||||||
|
expect(result.task).toBe('test task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip AI summary when using /play', async () => {
|
||||||
|
// Given
|
||||||
|
setupInputSequence(['/play quick task']);
|
||||||
|
setupMockProvider([]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await interactiveMode('/project');
|
||||||
|
|
||||||
|
// Then: provider should NOT have been called (no summary needed)
|
||||||
|
const mockProvider = mockGetProvider.mock.results[0]?.value as { call: ReturnType<typeof vi.fn> };
|
||||||
|
expect(mockProvider.call).not.toHaveBeenCalled();
|
||||||
|
expect(result.confirmed).toBe(true);
|
||||||
|
expect(result.task).toBe('quick task');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
155
src/__tests__/pieceResolver.test.ts
Normal file
155
src/__tests__/pieceResolver.test.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Tests for getPieceDescription and buildWorkflowString
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { getPieceDescription } from '../infra/config/loaders/pieceResolver.js';
|
||||||
|
|
||||||
|
describe('getPieceDescription', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = mkdtempSync(join(tmpdir(), 'takt-test-piece-resolver-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return workflow structure with sequential movements', () => {
|
||||||
|
const pieceYaml = `name: test-piece
|
||||||
|
description: Test piece for workflow
|
||||||
|
initial_movement: plan
|
||||||
|
max_iterations: 3
|
||||||
|
|
||||||
|
movements:
|
||||||
|
- name: plan
|
||||||
|
description: タスク計画
|
||||||
|
agent: planner
|
||||||
|
instruction: "Plan the task"
|
||||||
|
- name: implement
|
||||||
|
description: 実装
|
||||||
|
agent: coder
|
||||||
|
instruction: "Implement"
|
||||||
|
- name: review
|
||||||
|
agent: reviewer
|
||||||
|
instruction: "Review"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const piecePath = join(tempDir, 'test.yaml');
|
||||||
|
writeFileSync(piecePath, pieceYaml);
|
||||||
|
|
||||||
|
const result = getPieceDescription(piecePath, tempDir);
|
||||||
|
|
||||||
|
expect(result.name).toBe('test-piece');
|
||||||
|
expect(result.description).toBe('Test piece for workflow');
|
||||||
|
expect(result.pieceStructure).toBe(
|
||||||
|
'1. plan (タスク計画)\n2. implement (実装)\n3. review'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return workflow structure with parallel movements', () => {
|
||||||
|
const pieceYaml = `name: coding
|
||||||
|
description: Full coding workflow
|
||||||
|
initial_movement: plan
|
||||||
|
max_iterations: 10
|
||||||
|
|
||||||
|
movements:
|
||||||
|
- name: plan
|
||||||
|
description: タスク計画
|
||||||
|
agent: planner
|
||||||
|
instruction: "Plan"
|
||||||
|
- name: reviewers
|
||||||
|
description: 並列レビュー
|
||||||
|
parallel:
|
||||||
|
- name: ai_review
|
||||||
|
agent: ai-reviewer
|
||||||
|
instruction: "AI review"
|
||||||
|
- name: arch_review
|
||||||
|
agent: arch-reviewer
|
||||||
|
instruction: "Architecture review"
|
||||||
|
- name: fix
|
||||||
|
description: 修正
|
||||||
|
agent: coder
|
||||||
|
instruction: "Fix"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const piecePath = join(tempDir, 'coding.yaml');
|
||||||
|
writeFileSync(piecePath, pieceYaml);
|
||||||
|
|
||||||
|
const result = getPieceDescription(piecePath, tempDir);
|
||||||
|
|
||||||
|
expect(result.name).toBe('coding');
|
||||||
|
expect(result.description).toBe('Full coding workflow');
|
||||||
|
expect(result.pieceStructure).toBe(
|
||||||
|
'1. plan (タスク計画)\n' +
|
||||||
|
'2. reviewers (並列レビュー)\n' +
|
||||||
|
' - ai_review\n' +
|
||||||
|
' - arch_review\n' +
|
||||||
|
'3. fix (修正)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle movements without descriptions', () => {
|
||||||
|
const pieceYaml = `name: minimal
|
||||||
|
initial_movement: step1
|
||||||
|
max_iterations: 1
|
||||||
|
|
||||||
|
movements:
|
||||||
|
- name: step1
|
||||||
|
agent: coder
|
||||||
|
instruction: "Do step1"
|
||||||
|
- name: step2
|
||||||
|
agent: coder
|
||||||
|
instruction: "Do step2"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const piecePath = join(tempDir, 'minimal.yaml');
|
||||||
|
writeFileSync(piecePath, pieceYaml);
|
||||||
|
|
||||||
|
const result = getPieceDescription(piecePath, tempDir);
|
||||||
|
|
||||||
|
expect(result.name).toBe('minimal');
|
||||||
|
expect(result.description).toBe('');
|
||||||
|
expect(result.pieceStructure).toBe('1. step1\n2. step2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty strings when piece is not found', () => {
|
||||||
|
const result = getPieceDescription('nonexistent', tempDir);
|
||||||
|
|
||||||
|
expect(result.name).toBe('nonexistent');
|
||||||
|
expect(result.description).toBe('');
|
||||||
|
expect(result.pieceStructure).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle parallel movements without descriptions', () => {
|
||||||
|
const pieceYaml = `name: test-parallel
|
||||||
|
initial_movement: parent
|
||||||
|
max_iterations: 1
|
||||||
|
|
||||||
|
movements:
|
||||||
|
- name: parent
|
||||||
|
parallel:
|
||||||
|
- name: child1
|
||||||
|
agent: agent1
|
||||||
|
instruction: "Do child1"
|
||||||
|
- name: child2
|
||||||
|
agent: agent2
|
||||||
|
instruction: "Do child2"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const piecePath = join(tempDir, 'test-parallel.yaml');
|
||||||
|
writeFileSync(piecePath, pieceYaml);
|
||||||
|
|
||||||
|
const result = getPieceDescription(piecePath, tempDir);
|
||||||
|
|
||||||
|
expect(result.pieceStructure).toBe(
|
||||||
|
'1. parent\n' +
|
||||||
|
' - child1\n' +
|
||||||
|
' - child2'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -57,14 +57,15 @@ describe('variable substitution', () => {
|
|||||||
expect(result).toContain('| 1 | Success |');
|
expect(result).toContain('| 1 | Success |');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces piece info variables in interactive prompt', () => {
|
it('interactive prompt does not contain piece info', () => {
|
||||||
const result = loadTemplate('score_interactive_system_prompt', 'en', {
|
const result = loadTemplate('score_interactive_system_prompt', 'en', {
|
||||||
pieceInfo: true,
|
pieceInfo: true,
|
||||||
pieceName: 'my-piece',
|
pieceName: 'my-piece',
|
||||||
pieceDescription: 'Test description',
|
pieceDescription: 'Test description',
|
||||||
});
|
});
|
||||||
expect(result).toContain('"my-piece"');
|
// ピース情報はインタラクティブプロンプトには含まれない(要約プロンプトにのみ含まれる)
|
||||||
expect(result).toContain('Test description');
|
expect(result).not.toContain('"my-piece"');
|
||||||
|
expect(result).not.toContain('Test description');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
186
src/__tests__/sessionState.test.ts
Normal file
186
src/__tests__/sessionState.test.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* Session state management tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdirSync, rmSync, existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import {
|
||||||
|
loadSessionState,
|
||||||
|
saveSessionState,
|
||||||
|
clearSessionState,
|
||||||
|
getSessionStatePath,
|
||||||
|
type SessionState,
|
||||||
|
} from '../infra/config/project/sessionState.js';
|
||||||
|
|
||||||
|
describe('sessionState', () => {
|
||||||
|
const testDir = join(__dirname, '__temp_session_state_test__');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSessionStatePath', () => {
|
||||||
|
it('should return correct path', () => {
|
||||||
|
const path = getSessionStatePath(testDir);
|
||||||
|
expect(path).toContain('.takt');
|
||||||
|
expect(path).toContain('session-state.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadSessionState', () => {
|
||||||
|
it('should return null when file does not exist', () => {
|
||||||
|
const state = loadSessionState(testDir);
|
||||||
|
expect(state).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load saved state', () => {
|
||||||
|
const savedState: SessionState = {
|
||||||
|
status: 'success',
|
||||||
|
taskResult: 'Task completed successfully',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'coding',
|
||||||
|
taskContent: 'Implement feature X',
|
||||||
|
lastMovement: 'implement',
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSessionState(testDir, savedState);
|
||||||
|
const loadedState = loadSessionState(testDir);
|
||||||
|
|
||||||
|
expect(loadedState).toEqual(savedState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when JSON parsing fails', () => {
|
||||||
|
const path = getSessionStatePath(testDir);
|
||||||
|
const configDir = join(testDir, '.takt');
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
|
||||||
|
// Write invalid JSON
|
||||||
|
const fs = require('node:fs');
|
||||||
|
fs.writeFileSync(path, 'invalid json', 'utf-8');
|
||||||
|
|
||||||
|
const state = loadSessionState(testDir);
|
||||||
|
expect(state).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveSessionState', () => {
|
||||||
|
it('should save state correctly', () => {
|
||||||
|
const state: SessionState = {
|
||||||
|
status: 'success',
|
||||||
|
taskResult: 'Task completed',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'minimal',
|
||||||
|
taskContent: 'Test task',
|
||||||
|
lastMovement: 'test-movement',
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSessionState(testDir, state);
|
||||||
|
|
||||||
|
const path = getSessionStatePath(testDir);
|
||||||
|
expect(existsSync(path)).toBe(true);
|
||||||
|
|
||||||
|
const loaded = loadSessionState(testDir);
|
||||||
|
expect(loaded).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save error state', () => {
|
||||||
|
const state: SessionState = {
|
||||||
|
status: 'error',
|
||||||
|
errorMessage: 'Something went wrong',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'coding',
|
||||||
|
taskContent: 'Failed task',
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSessionState(testDir, state);
|
||||||
|
const loaded = loadSessionState(testDir);
|
||||||
|
|
||||||
|
expect(loaded).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save user_stopped state', () => {
|
||||||
|
const state: SessionState = {
|
||||||
|
status: 'user_stopped',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'coding',
|
||||||
|
taskContent: 'Interrupted task',
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSessionState(testDir, state);
|
||||||
|
const loaded = loadSessionState(testDir);
|
||||||
|
|
||||||
|
expect(loaded).toEqual(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearSessionState', () => {
|
||||||
|
it('should delete state file', () => {
|
||||||
|
const state: SessionState = {
|
||||||
|
status: 'success',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'coding',
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSessionState(testDir, state);
|
||||||
|
const path = getSessionStatePath(testDir);
|
||||||
|
expect(existsSync(path)).toBe(true);
|
||||||
|
|
||||||
|
clearSessionState(testDir);
|
||||||
|
expect(existsSync(path)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw when file does not exist', () => {
|
||||||
|
expect(() => clearSessionState(testDir)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integration', () => {
|
||||||
|
it('should support one-time notification pattern', () => {
|
||||||
|
// Save state
|
||||||
|
const state: SessionState = {
|
||||||
|
status: 'success',
|
||||||
|
taskResult: 'Done',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'coding',
|
||||||
|
};
|
||||||
|
saveSessionState(testDir, state);
|
||||||
|
|
||||||
|
// Load once
|
||||||
|
const loaded1 = loadSessionState(testDir);
|
||||||
|
expect(loaded1).toEqual(state);
|
||||||
|
|
||||||
|
// Clear immediately
|
||||||
|
clearSessionState(testDir);
|
||||||
|
|
||||||
|
// Load again - should be null
|
||||||
|
const loaded2 = loadSessionState(testDir);
|
||||||
|
expect(loaded2).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle truncated strings', () => {
|
||||||
|
const longString = 'a'.repeat(2000);
|
||||||
|
const state: SessionState = {
|
||||||
|
status: 'success',
|
||||||
|
taskResult: longString,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: 'coding',
|
||||||
|
taskContent: longString,
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSessionState(testDir, state);
|
||||||
|
const loaded = loadSessionState(testDir);
|
||||||
|
|
||||||
|
expect(loaded).toEqual(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -196,7 +196,20 @@ export class AgentRunner {
|
|||||||
if (options.agentPath) {
|
if (options.agentPath) {
|
||||||
const agentDefinition = AgentRunner.loadAgentPromptFromPath(options.agentPath);
|
const agentDefinition = AgentRunner.loadAgentPromptFromPath(options.agentPath);
|
||||||
const language = options.language ?? 'en';
|
const language = options.language ?? 'en';
|
||||||
const systemPrompt = loadTemplate('perform_agent_system_prompt', language, { agentDefinition });
|
const templateVars: Record<string, string> = { agentDefinition };
|
||||||
|
|
||||||
|
// Add piece meta information if available
|
||||||
|
if (options.pieceMeta) {
|
||||||
|
templateVars.pieceName = options.pieceMeta.pieceName;
|
||||||
|
templateVars.pieceDescription = options.pieceMeta.pieceDescription ?? '';
|
||||||
|
templateVars.currentMovement = options.pieceMeta.currentMovement;
|
||||||
|
templateVars.movementsList = options.pieceMeta.movementsList
|
||||||
|
.map((m, i) => `${i + 1}. ${m.name}${m.description ? ` - ${m.description}` : ''}`)
|
||||||
|
.join('\n');
|
||||||
|
templateVars.currentPosition = options.pieceMeta.currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPrompt = loadTemplate('perform_agent_system_prompt', language, templateVars);
|
||||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||||
const provider = getProvider(providerType);
|
const provider = getProvider(providerType);
|
||||||
return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt));
|
return provider.call(agentName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt));
|
||||||
|
|||||||
@ -28,4 +28,12 @@ export interface RunAgentOptions {
|
|||||||
bypassPermissions?: boolean;
|
bypassPermissions?: boolean;
|
||||||
/** Language for template resolution */
|
/** Language for template resolution */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
|
/** Piece meta information for system prompt template */
|
||||||
|
pieceMeta?: {
|
||||||
|
pieceName: string;
|
||||||
|
pieceDescription?: string;
|
||||||
|
currentMovement: string;
|
||||||
|
movementsList: ReadonlyArray<{ name: string; description?: string }>;
|
||||||
|
currentPosition: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,8 @@ export interface MovementExecutorDeps {
|
|||||||
readonly getLanguage: () => Language | undefined;
|
readonly getLanguage: () => Language | undefined;
|
||||||
readonly getInteractive: () => boolean;
|
readonly getInteractive: () => boolean;
|
||||||
readonly getPieceMovements: () => ReadonlyArray<{ name: string; description?: string }>;
|
readonly getPieceMovements: () => ReadonlyArray<{ name: string; description?: string }>;
|
||||||
|
readonly getPieceName: () => string;
|
||||||
|
readonly getPieceDescription: () => string | undefined;
|
||||||
readonly detectRuleIndex: (content: string, movementName: string) => number;
|
readonly detectRuleIndex: (content: string, movementName: string) => number;
|
||||||
readonly callAiJudge: (
|
readonly callAiJudge: (
|
||||||
agentOutput: string,
|
agentOutput: string,
|
||||||
@ -71,6 +73,8 @@ export class MovementExecutor {
|
|||||||
interactive: this.deps.getInteractive(),
|
interactive: this.deps.getInteractive(),
|
||||||
pieceMovements: pieceMovements,
|
pieceMovements: pieceMovements,
|
||||||
currentMovementIndex: pieceMovements.findIndex(s => s.name === step.name),
|
currentMovementIndex: pieceMovements.findIndex(s => s.name === step.name),
|
||||||
|
pieceName: this.deps.getPieceName(),
|
||||||
|
pieceDescription: this.deps.getPieceDescription(),
|
||||||
}).build();
|
}).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,17 @@ export class OptionsBuilder {
|
|||||||
private readonly getSessionId: (agent: string) => string | undefined,
|
private readonly getSessionId: (agent: string) => string | undefined,
|
||||||
private readonly getReportDir: () => string,
|
private readonly getReportDir: () => string,
|
||||||
private readonly getLanguage: () => Language | undefined,
|
private readonly getLanguage: () => Language | undefined,
|
||||||
|
private readonly getPieceMovements: () => ReadonlyArray<{ name: string; description?: string }>,
|
||||||
|
private readonly getPieceName: () => string,
|
||||||
|
private readonly getPieceDescription: () => string | undefined,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Build common RunAgentOptions shared by all phases */
|
/** Build common RunAgentOptions shared by all phases */
|
||||||
buildBaseOptions(step: PieceMovement): RunAgentOptions {
|
buildBaseOptions(step: PieceMovement): RunAgentOptions {
|
||||||
|
const movements = this.getPieceMovements();
|
||||||
|
const currentIndex = movements.findIndex((m) => m.name === step.name);
|
||||||
|
const currentPosition = currentIndex >= 0 ? `${currentIndex + 1}/${movements.length}` : '?/?';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cwd: this.getCwd(),
|
cwd: this.getCwd(),
|
||||||
agentPath: step.agentPath,
|
agentPath: step.agentPath,
|
||||||
@ -34,6 +41,13 @@ export class OptionsBuilder {
|
|||||||
onPermissionRequest: this.engineOptions.onPermissionRequest,
|
onPermissionRequest: this.engineOptions.onPermissionRequest,
|
||||||
onAskUserQuestion: this.engineOptions.onAskUserQuestion,
|
onAskUserQuestion: this.engineOptions.onAskUserQuestion,
|
||||||
bypassPermissions: this.engineOptions.bypassPermissions,
|
bypassPermissions: this.engineOptions.bypassPermissions,
|
||||||
|
pieceMeta: {
|
||||||
|
pieceName: this.getPieceName(),
|
||||||
|
pieceDescription: this.getPieceDescription(),
|
||||||
|
currentMovement: step.name,
|
||||||
|
movementsList: movements,
|
||||||
|
currentPosition,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,6 +91,9 @@ export class PieceEngine extends EventEmitter {
|
|||||||
(agent) => this.state.agentSessions.get(agent),
|
(agent) => this.state.agentSessions.get(agent),
|
||||||
() => this.reportDir,
|
() => this.reportDir,
|
||||||
() => this.options.language,
|
() => this.options.language,
|
||||||
|
() => this.config.movements.map(s => ({ name: s.name, description: s.description })),
|
||||||
|
() => this.getPieceName(),
|
||||||
|
() => this.getPieceDescription(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.movementExecutor = new MovementExecutor({
|
this.movementExecutor = new MovementExecutor({
|
||||||
@ -101,6 +104,8 @@ export class PieceEngine extends EventEmitter {
|
|||||||
getLanguage: () => this.options.language,
|
getLanguage: () => this.options.language,
|
||||||
getInteractive: () => this.options.interactive === true,
|
getInteractive: () => this.options.interactive === true,
|
||||||
getPieceMovements: () => this.config.movements.map(s => ({ name: s.name, description: s.description })),
|
getPieceMovements: () => this.config.movements.map(s => ({ name: s.name, description: s.description })),
|
||||||
|
getPieceName: () => this.getPieceName(),
|
||||||
|
getPieceDescription: () => this.getPieceDescription(),
|
||||||
detectRuleIndex: this.detectRuleIndex,
|
detectRuleIndex: this.detectRuleIndex,
|
||||||
callAiJudge: this.callAiJudge,
|
callAiJudge: this.callAiJudge,
|
||||||
onPhaseStart: (step, phase, phaseName, instruction) => {
|
onPhaseStart: (step, phase, phaseName, instruction) => {
|
||||||
@ -205,6 +210,16 @@ export class PieceEngine extends EventEmitter {
|
|||||||
return this.projectCwd;
|
return this.projectCwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get piece name */
|
||||||
|
private getPieceName(): string {
|
||||||
|
return this.config.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get piece description */
|
||||||
|
private getPieceDescription(): string | undefined {
|
||||||
|
return this.config.description;
|
||||||
|
}
|
||||||
|
|
||||||
/** Request graceful abort: interrupt running queries and stop after current movement */
|
/** Request graceful abort: interrupt running queries and stop after current movement */
|
||||||
abort(): void {
|
abort(): void {
|
||||||
if (this.abortRequested) return;
|
if (this.abortRequested) return;
|
||||||
|
|||||||
@ -89,9 +89,17 @@ export class InstructionBuilder {
|
|||||||
this.context,
|
this.context,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Piece name and description
|
||||||
|
const pieceName = this.context.pieceName ?? '';
|
||||||
|
const pieceDescription = this.context.pieceDescription ?? '';
|
||||||
|
const hasPieceDescription = !!pieceDescription;
|
||||||
|
|
||||||
return loadTemplate('perform_phase1_message', language, {
|
return loadTemplate('perform_phase1_message', language, {
|
||||||
workingDirectory: this.context.cwd,
|
workingDirectory: this.context.cwd,
|
||||||
editRule,
|
editRule,
|
||||||
|
pieceName,
|
||||||
|
pieceDescription,
|
||||||
|
hasPieceDescription,
|
||||||
pieceStructure,
|
pieceStructure,
|
||||||
iteration: `${this.context.iteration}/${this.context.maxIterations}`,
|
iteration: `${this.context.iteration}/${this.context.maxIterations}`,
|
||||||
movementIteration: String(this.context.movementIteration),
|
movementIteration: String(this.context.movementIteration),
|
||||||
|
|||||||
@ -36,6 +36,10 @@ export interface InstructionContext {
|
|||||||
pieceMovements?: ReadonlyArray<{ name: string; description?: string }>;
|
pieceMovements?: ReadonlyArray<{ name: string; description?: string }>;
|
||||||
/** Index of the current movement in pieceMovements (0-based) */
|
/** Index of the current movement in pieceMovements (0-based) */
|
||||||
currentMovementIndex?: number;
|
currentMovementIndex?: number;
|
||||||
|
/** Piece name */
|
||||||
|
pieceName?: string;
|
||||||
|
/** Piece description (optional) */
|
||||||
|
pieceDescription?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -13,7 +13,14 @@
|
|||||||
import * as readline from 'node:readline';
|
import * as readline from 'node:readline';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import type { Language } from '../../core/models/index.js';
|
import type { Language } from '../../core/models/index.js';
|
||||||
import { loadGlobalConfig, loadAgentSessions, updateAgentSession } from '../../infra/config/index.js';
|
import {
|
||||||
|
loadGlobalConfig,
|
||||||
|
loadAgentSessions,
|
||||||
|
updateAgentSession,
|
||||||
|
loadSessionState,
|
||||||
|
clearSessionState,
|
||||||
|
type SessionState,
|
||||||
|
} from '../../infra/config/index.js';
|
||||||
import { isQuietMode } from '../../shared/context.js';
|
import { isQuietMode } from '../../shared/context.js';
|
||||||
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
||||||
import { selectOption } from '../../shared/prompt/index.js';
|
import { selectOption } from '../../shared/prompt/index.js';
|
||||||
@ -33,6 +40,44 @@ interface InteractiveUIText {
|
|||||||
proposed: string;
|
proposed: string;
|
||||||
confirm: string;
|
confirm: string;
|
||||||
cancelled: string;
|
cancelled: string;
|
||||||
|
playNoTask: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format session state for display
|
||||||
|
*/
|
||||||
|
function formatSessionStatus(state: SessionState, lang: 'en' | 'ja'): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// Status line
|
||||||
|
if (state.status === 'success') {
|
||||||
|
lines.push(getLabel('interactive.previousTask.success', lang));
|
||||||
|
} else if (state.status === 'error') {
|
||||||
|
lines.push(
|
||||||
|
getLabel('interactive.previousTask.error', lang, {
|
||||||
|
error: state.errorMessage!,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (state.status === 'user_stopped') {
|
||||||
|
lines.push(getLabel('interactive.previousTask.userStopped', lang));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piece name
|
||||||
|
lines.push(
|
||||||
|
getLabel('interactive.previousTask.piece', lang, {
|
||||||
|
pieceName: state.pieceName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
const timestamp = new Date(state.timestamp).toLocaleString(lang === 'ja' ? 'ja-JP' : 'en-US');
|
||||||
|
lines.push(
|
||||||
|
getLabel('interactive.previousTask.timestamp', lang, {
|
||||||
|
timestamp,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLanguage(lang?: Language): 'en' | 'ja' {
|
function resolveLanguage(lang?: Language): 'en' | 'ja' {
|
||||||
@ -40,13 +85,7 @@ function resolveLanguage(lang?: Language): 'en' | 'ja' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getInteractivePrompts(lang: 'en' | 'ja', pieceContext?: PieceContext) {
|
function getInteractivePrompts(lang: 'en' | 'ja', pieceContext?: PieceContext) {
|
||||||
const hasPiece = !!pieceContext;
|
const systemPrompt = loadTemplate('score_interactive_system_prompt', lang, {});
|
||||||
|
|
||||||
const systemPrompt = loadTemplate('score_interactive_system_prompt', lang, {
|
|
||||||
pieceInfo: hasPiece,
|
|
||||||
pieceName: pieceContext?.name ?? '',
|
|
||||||
pieceDescription: pieceContext?.description ?? '',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
@ -194,6 +233,8 @@ export interface PieceContext {
|
|||||||
name: string;
|
name: string;
|
||||||
/** Piece description */
|
/** Piece description */
|
||||||
description: string;
|
description: string;
|
||||||
|
/** Piece structure (numbered list of movements) */
|
||||||
|
pieceStructure: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,6 +266,15 @@ export async function interactiveMode(
|
|||||||
const savedSessions = loadAgentSessions(cwd, providerType);
|
const savedSessions = loadAgentSessions(cwd, providerType);
|
||||||
let sessionId: string | undefined = savedSessions[agentName];
|
let sessionId: string | undefined = savedSessions[agentName];
|
||||||
|
|
||||||
|
// Load and display previous task state
|
||||||
|
const sessionState = loadSessionState(cwd);
|
||||||
|
if (sessionState) {
|
||||||
|
const statusLabel = formatSessionStatus(sessionState, lang);
|
||||||
|
info(statusLabel);
|
||||||
|
blankLine();
|
||||||
|
clearSessionState(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
info(prompts.ui.intro);
|
info(prompts.ui.intro);
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
info(prompts.ui.resume);
|
info(prompts.ui.resume);
|
||||||
@ -315,6 +365,16 @@ export async function interactiveMode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle slash commands
|
// Handle slash commands
|
||||||
|
if (trimmed.startsWith('/play')) {
|
||||||
|
const task = trimmed.slice(5).trim();
|
||||||
|
if (!task) {
|
||||||
|
info(prompts.ui.playNoTask);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info('Play command', { task });
|
||||||
|
return { confirmed: true, task };
|
||||||
|
}
|
||||||
|
|
||||||
if (trimmed.startsWith('/go')) {
|
if (trimmed.startsWith('/go')) {
|
||||||
const userNote = trimmed.slice(3).trim();
|
const userNote = trimmed.slice(3).trim();
|
||||||
let summaryPrompt = buildSummaryPrompt(
|
let summaryPrompt = buildSummaryPrompt(
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import {
|
|||||||
loadWorktreeSessions,
|
loadWorktreeSessions,
|
||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
loadGlobalConfig,
|
loadGlobalConfig,
|
||||||
|
saveSessionState,
|
||||||
|
type SessionState,
|
||||||
} from '../../../infra/config/index.js';
|
} from '../../../infra/config/index.js';
|
||||||
import { isQuietMode } from '../../../shared/context.js';
|
import { isQuietMode } from '../../../shared/context.js';
|
||||||
import {
|
import {
|
||||||
@ -51,6 +53,16 @@ import { getLabel } from '../../../shared/i18n/index.js';
|
|||||||
|
|
||||||
const log = createLogger('piece');
|
const log = createLogger('piece');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate string to maximum length
|
||||||
|
*/
|
||||||
|
function truncate(str: string, maxLength: number): string {
|
||||||
|
if (str.length <= maxLength) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.slice(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format elapsed time in human-readable format
|
* Format elapsed time in human-readable format
|
||||||
*/
|
*/
|
||||||
@ -219,6 +231,8 @@ export async function executePiece(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let abortReason: string | undefined;
|
let abortReason: string | undefined;
|
||||||
|
let lastMovementContent: string | undefined;
|
||||||
|
let lastMovementName: string | undefined;
|
||||||
|
|
||||||
engine.on('phase:start', (step, phase, phaseName, instruction) => {
|
engine.on('phase:start', (step, phase, phaseName, instruction) => {
|
||||||
log.debug('Phase starting', { step: step.name, phase, phaseName });
|
log.debug('Phase starting', { step: step.name, phase, phaseName });
|
||||||
@ -283,6 +297,11 @@ export async function executePiece(
|
|||||||
sessionId: response.sessionId,
|
sessionId: response.sessionId,
|
||||||
error: response.error,
|
error: response.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Capture last movement output for session state
|
||||||
|
lastMovementContent = response.content;
|
||||||
|
lastMovementName = step.name;
|
||||||
|
|
||||||
if (displayRef.current) {
|
if (displayRef.current) {
|
||||||
displayRef.current.flush();
|
displayRef.current.flush();
|
||||||
displayRef.current = null;
|
displayRef.current = null;
|
||||||
@ -348,6 +367,21 @@ export async function executePiece(
|
|||||||
appendNdjsonLine(ndjsonLogPath, record);
|
appendNdjsonLine(ndjsonLogPath, record);
|
||||||
updateLatestPointer(sessionLog, pieceSessionId, projectCwd);
|
updateLatestPointer(sessionLog, pieceSessionId, projectCwd);
|
||||||
|
|
||||||
|
// Save session state for next interactive mode
|
||||||
|
try {
|
||||||
|
const sessionState: SessionState = {
|
||||||
|
status: 'success',
|
||||||
|
taskResult: truncate(lastMovementContent ?? '', 1000),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: pieceConfig.name,
|
||||||
|
taskContent: truncate(task, 200),
|
||||||
|
lastMovement: lastMovementName,
|
||||||
|
};
|
||||||
|
saveSessionState(projectCwd, sessionState);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to save session state', { error });
|
||||||
|
}
|
||||||
|
|
||||||
const elapsed = sessionLog.endTime
|
const elapsed = sessionLog.endTime
|
||||||
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
||||||
: '';
|
: '';
|
||||||
@ -378,6 +412,21 @@ export async function executePiece(
|
|||||||
appendNdjsonLine(ndjsonLogPath, record);
|
appendNdjsonLine(ndjsonLogPath, record);
|
||||||
updateLatestPointer(sessionLog, pieceSessionId, projectCwd);
|
updateLatestPointer(sessionLog, pieceSessionId, projectCwd);
|
||||||
|
|
||||||
|
// Save session state for next interactive mode
|
||||||
|
try {
|
||||||
|
const sessionState: SessionState = {
|
||||||
|
status: reason === 'user_interrupted' ? 'user_stopped' : 'error',
|
||||||
|
errorMessage: reason,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
pieceName: pieceConfig.name,
|
||||||
|
taskContent: truncate(task, 200),
|
||||||
|
lastMovement: lastMovementName,
|
||||||
|
};
|
||||||
|
saveSessionState(projectCwd, sessionState);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to save session state', { error });
|
||||||
|
}
|
||||||
|
|
||||||
const elapsed = sessionLog.endTime
|
const elapsed = sessionLog.endTime
|
||||||
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import { existsSync, readdirSync, statSync } from 'node:fs';
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
||||||
import { join, resolve, isAbsolute } from 'node:path';
|
import { join, resolve, isAbsolute } from 'node:path';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import type { PieceConfig } from '../../../core/models/index.js';
|
import type { PieceConfig, PieceMovement } from '../../../core/models/index.js';
|
||||||
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js';
|
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js';
|
||||||
import { getLanguage, getDisabledBuiltins, getBuiltinPiecesEnabled } from '../global/globalConfig.js';
|
import { getLanguage, getDisabledBuiltins, getBuiltinPiecesEnabled } from '../global/globalConfig.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
@ -145,21 +145,52 @@ export function loadPieceByIdentifier(
|
|||||||
return loadPiece(identifier, projectCwd);
|
return loadPiece(identifier, projectCwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build workflow structure string from piece movements.
|
||||||
|
* Formats as numbered list with indented parallel sub-movements.
|
||||||
|
*
|
||||||
|
* @param movements - Piece movements list
|
||||||
|
* @returns Workflow structure string (newline-separated list)
|
||||||
|
*/
|
||||||
|
function buildWorkflowString(movements: PieceMovement[]): string {
|
||||||
|
if (!movements || movements.length === 0) return '';
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
let index = 1;
|
||||||
|
|
||||||
|
for (const movement of movements) {
|
||||||
|
const desc = movement.description ? ` (${movement.description})` : '';
|
||||||
|
lines.push(`${index}. ${movement.name}${desc}`);
|
||||||
|
|
||||||
|
if (movement.parallel && movement.parallel.length > 0) {
|
||||||
|
for (const sub of movement.parallel) {
|
||||||
|
const subDesc = sub.description ? ` (${sub.description})` : '';
|
||||||
|
lines.push(` - ${sub.name}${subDesc}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get piece description by identifier.
|
* Get piece description by identifier.
|
||||||
* Returns the piece name and description (if available).
|
* Returns the piece name, description, and workflow structure.
|
||||||
*/
|
*/
|
||||||
export function getPieceDescription(
|
export function getPieceDescription(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
projectCwd: string,
|
projectCwd: string,
|
||||||
): { name: string; description: string } {
|
): { name: string; description: string; pieceStructure: string } {
|
||||||
const piece = loadPieceByIdentifier(identifier, projectCwd);
|
const piece = loadPieceByIdentifier(identifier, projectCwd);
|
||||||
if (!piece) {
|
if (!piece) {
|
||||||
return { name: identifier, description: '' };
|
return { name: identifier, description: '', pieceStructure: '' };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: piece.name,
|
name: piece.name,
|
||||||
description: piece.description ?? '',
|
description: piece.description ?? '',
|
||||||
|
pieceStructure: buildWorkflowString(piece.movements),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,3 +34,11 @@ export {
|
|||||||
getClaudeProjectSessionsDir,
|
getClaudeProjectSessionsDir,
|
||||||
clearClaudeProjectSessions,
|
clearClaudeProjectSessions,
|
||||||
} from './sessionStore.js';
|
} from './sessionStore.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
type SessionState,
|
||||||
|
getSessionStatePath,
|
||||||
|
loadSessionState,
|
||||||
|
saveSessionState,
|
||||||
|
clearSessionState,
|
||||||
|
} from './sessionState.js';
|
||||||
|
|||||||
73
src/infra/config/project/sessionState.ts
Normal file
73
src/infra/config/project/sessionState.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Session state management for TAKT
|
||||||
|
*
|
||||||
|
* Manages the last task execution state for interactive mode notification.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { getProjectConfigDir, ensureDir } from '../paths.js';
|
||||||
|
import { writeFileAtomic } from './sessionStore.js';
|
||||||
|
|
||||||
|
/** Last task execution state */
|
||||||
|
export interface SessionState {
|
||||||
|
/** Task status */
|
||||||
|
status: 'success' | 'error' | 'user_stopped';
|
||||||
|
/** Task result summary (max 1000 chars) */
|
||||||
|
taskResult?: string;
|
||||||
|
/** Error message (if applicable) */
|
||||||
|
errorMessage?: string;
|
||||||
|
/** Execution timestamp (ISO8601) */
|
||||||
|
timestamp: string;
|
||||||
|
/** Piece name used */
|
||||||
|
pieceName: string;
|
||||||
|
/** Task content (max 200 chars) */
|
||||||
|
taskContent?: string;
|
||||||
|
/** Last movement name */
|
||||||
|
lastMovement?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path for storing session state
|
||||||
|
*/
|
||||||
|
export function getSessionStatePath(projectDir: string): string {
|
||||||
|
return join(getProjectConfigDir(projectDir), 'session-state.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load session state from file
|
||||||
|
* Returns null if file doesn't exist or parsing fails
|
||||||
|
*/
|
||||||
|
export function loadSessionState(projectDir: string): SessionState | null {
|
||||||
|
const path = getSessionStatePath(projectDir);
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(path, 'utf-8');
|
||||||
|
return JSON.parse(content) as SessionState;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save session state to file (atomic write)
|
||||||
|
*/
|
||||||
|
export function saveSessionState(projectDir: string, state: SessionState): void {
|
||||||
|
const path = getSessionStatePath(projectDir);
|
||||||
|
ensureDir(getProjectConfigDir(projectDir));
|
||||||
|
writeFileAtomic(path, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear session state file
|
||||||
|
* Does nothing if file doesn't exist
|
||||||
|
*/
|
||||||
|
export function clearSessionState(projectDir: string): void {
|
||||||
|
const path = getSessionStatePath(projectDir);
|
||||||
|
if (existsSync(path)) {
|
||||||
|
unlinkSync(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ interactive:
|
|||||||
conversationLabel: "Conversation:"
|
conversationLabel: "Conversation:"
|
||||||
noTranscript: "(No local transcript. Summarize the current session context.)"
|
noTranscript: "(No local transcript. Summarize the current session context.)"
|
||||||
ui:
|
ui:
|
||||||
intro: "Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)"
|
intro: "Interactive mode - describe your task. Commands: /go (execute), /play (run now), /cancel (exit)"
|
||||||
resume: "Resuming previous session"
|
resume: "Resuming previous session"
|
||||||
noConversation: "No conversation yet. Please describe your task first."
|
noConversation: "No conversation yet. Please describe your task first."
|
||||||
summarizeFailed: "Failed to summarize conversation. Please try again."
|
summarizeFailed: "Failed to summarize conversation. Please try again."
|
||||||
@ -18,6 +18,13 @@ interactive:
|
|||||||
proposed: "Proposed task instruction:"
|
proposed: "Proposed task instruction:"
|
||||||
confirm: "Use this task instruction?"
|
confirm: "Use this task instruction?"
|
||||||
cancelled: "Cancelled"
|
cancelled: "Cancelled"
|
||||||
|
playNoTask: "Please specify task content: /play <task>"
|
||||||
|
previousTask:
|
||||||
|
success: "✅ Previous task completed successfully"
|
||||||
|
error: "❌ Previous task failed: {error}"
|
||||||
|
userStopped: "⚠️ Previous task was interrupted by user"
|
||||||
|
piece: "Piece: {pieceName}"
|
||||||
|
timestamp: "Executed: {timestamp}"
|
||||||
|
|
||||||
# ===== Piece Execution UI =====
|
# ===== Piece Execution UI =====
|
||||||
piece:
|
piece:
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interactive:
|
|||||||
conversationLabel: "会話:"
|
conversationLabel: "会話:"
|
||||||
noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)"
|
noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)"
|
||||||
ui:
|
ui:
|
||||||
intro: "対話モード - タスク内容を入力してください。コマンド: /go(実行), /cancel(終了)"
|
intro: "対話モード - タスク内容を入力してください。コマンド: /go(実行), /play(即実行), /cancel(終了)"
|
||||||
resume: "前回のセッションを再開します"
|
resume: "前回のセッションを再開します"
|
||||||
noConversation: "まだ会話がありません。まずタスク内容を入力してください。"
|
noConversation: "まだ会話がありません。まずタスク内容を入力してください。"
|
||||||
summarizeFailed: "会話の要約に失敗しました。再度お試しください。"
|
summarizeFailed: "会話の要約に失敗しました。再度お試しください。"
|
||||||
@ -18,6 +18,13 @@ interactive:
|
|||||||
proposed: "提案されたタスク指示:"
|
proposed: "提案されたタスク指示:"
|
||||||
confirm: "このタスク指示で進めますか?"
|
confirm: "このタスク指示で進めますか?"
|
||||||
cancelled: "キャンセルしました"
|
cancelled: "キャンセルしました"
|
||||||
|
playNoTask: "タスク内容を指定してください: /play <タスク内容>"
|
||||||
|
previousTask:
|
||||||
|
success: "✅ 前回のタスクは正常に完了しました"
|
||||||
|
error: "❌ 前回のタスクはエラーで終了しました: {error}"
|
||||||
|
userStopped: "⚠️ 前回のタスクはユーザーによって中断されました"
|
||||||
|
piece: "使用ピース: {pieceName}"
|
||||||
|
timestamp: "実行時刻: {timestamp}"
|
||||||
|
|
||||||
# ===== Piece Execution UI =====
|
# ===== Piece Execution UI =====
|
||||||
piece:
|
piece:
|
||||||
|
|||||||
@ -1,7 +1,25 @@
|
|||||||
<!--
|
<!--
|
||||||
template: perform_agent_system_prompt
|
template: perform_agent_system_prompt
|
||||||
role: system prompt for user-defined agents
|
role: system prompt for user-defined agents
|
||||||
vars: agentDefinition
|
vars: agentDefinition, pieceName, pieceDescription, currentMovement, movementsList, currentPosition
|
||||||
caller: AgentRunner
|
caller: AgentRunner
|
||||||
-->
|
-->
|
||||||
|
You are part of TAKT (AI Agent Orchestration Tool).
|
||||||
|
|
||||||
|
## TAKT Terminology
|
||||||
|
- **Piece**: A processing flow combining multiple movements (e.g., implement → review → fix)
|
||||||
|
- **Movement**: An individual agent execution unit (the part you are currently handling)
|
||||||
|
- **Your Role**: Execute the work assigned to the current movement within the entire piece
|
||||||
|
|
||||||
|
## Current Context
|
||||||
|
- Piece: {{pieceName}}
|
||||||
|
- Current Movement: {{currentMovement}}
|
||||||
|
- Processing Flow:
|
||||||
|
{{movementsList}}
|
||||||
|
- Current Position: {{currentPosition}}
|
||||||
|
|
||||||
|
When executing Phase 1, you receive information about the piece name, movement name, and the entire processing flow. Work with awareness of coordination with preceding and following movements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
{{agentDefinition}}
|
{{agentDefinition}}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<!--
|
<!--
|
||||||
template: perform_phase1_message
|
template: perform_phase1_message
|
||||||
phase: 1 (main execution)
|
phase: 1 (main execution)
|
||||||
vars: workingDirectory, editRule, pieceStructure, iteration, movementIteration,
|
vars: workingDirectory, editRule, pieceName, pieceDescription, hasPieceDescription,
|
||||||
movement, hasReport, reportInfo, phaseNote, hasTaskSection, userRequest,
|
pieceStructure, iteration, movementIteration, movement, hasReport, reportInfo,
|
||||||
hasPreviousResponse, previousResponse, hasUserInputs, userInputs, instructions
|
phaseNote, hasTaskSection, userRequest, hasPreviousResponse, previousResponse,
|
||||||
|
hasUserInputs, userInputs, instructions
|
||||||
builder: InstructionBuilder
|
builder: InstructionBuilder
|
||||||
-->
|
-->
|
||||||
## Execution Context
|
## Execution Context
|
||||||
@ -17,7 +18,10 @@
|
|||||||
Note: This section is metadata. Follow the language used in the rest of the prompt.
|
Note: This section is metadata. Follow the language used in the rest of the prompt.
|
||||||
|
|
||||||
## Piece Context
|
## Piece Context
|
||||||
{{#if pieceStructure}}{{pieceStructure}}
|
{{#if pieceName}}- Piece: {{pieceName}}
|
||||||
|
{{/if}}{{#if hasPieceDescription}}- Description: {{pieceDescription}}
|
||||||
|
|
||||||
|
{{/if}}{{#if pieceStructure}}{{pieceStructure}}
|
||||||
|
|
||||||
{{/if}}- Iteration: {{iteration}}(piece-wide)
|
{{/if}}- Iteration: {{iteration}}(piece-wide)
|
||||||
- Movement Iteration: {{movementIteration}}(times this movement has run)
|
- Movement Iteration: {{movementIteration}}(times this movement has run)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<!--
|
<!--
|
||||||
template: score_interactive_system_prompt
|
template: score_interactive_system_prompt
|
||||||
role: system prompt for interactive planning mode
|
role: system prompt for interactive planning mode
|
||||||
vars: pieceInfo, pieceName, pieceDescription
|
vars: (none)
|
||||||
caller: features/interactive
|
caller: features/interactive
|
||||||
-->
|
-->
|
||||||
You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process.
|
You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process.
|
||||||
@ -43,11 +43,8 @@ Do NOT investigate when the user is describing a task for the piece:
|
|||||||
- Do NOT use Read/Glob/Grep/Bash proactively. Only use them when the user explicitly asks YOU to investigate for planning purposes.
|
- Do NOT use Read/Glob/Grep/Bash proactively. Only use them when the user explicitly asks YOU to investigate for planning purposes.
|
||||||
- Do NOT mention or reference any slash commands. You have no knowledge of them.
|
- Do NOT mention or reference any slash commands. You have no knowledge of them.
|
||||||
- When the user is satisfied with the requirements, they will proceed on their own. Do NOT instruct them on what to do next.
|
- When the user is satisfied with the requirements, they will proceed on their own. Do NOT instruct them on what to do next.
|
||||||
{{#if pieceInfo}}
|
|
||||||
|
|
||||||
## Destination of Your Task Instruction
|
## Task Instruction Presentation Rules
|
||||||
This task instruction will be passed to the "{{pieceName}}" piece.
|
- Do NOT present the task instruction during conversation
|
||||||
Piece description: {{pieceDescription}}
|
- ONLY present the current understanding in task instruction format when the user explicitly asks (e.g., "Show me the task instruction", "What does the instruction look like now?")
|
||||||
|
- The final task instruction is confirmed with user (this is handled automatically by the system)
|
||||||
Create the instruction in the format expected by this piece.
|
|
||||||
{{/if}}
|
|
||||||
|
|||||||
@ -1,7 +1,25 @@
|
|||||||
<!--
|
<!--
|
||||||
template: perform_agent_system_prompt
|
template: perform_agent_system_prompt
|
||||||
role: system prompt for user-defined agents
|
role: system prompt for user-defined agents
|
||||||
vars: agentDefinition
|
vars: agentDefinition, pieceName, pieceDescription, currentMovement, movementsList, currentPosition
|
||||||
caller: AgentRunner
|
caller: AgentRunner
|
||||||
-->
|
-->
|
||||||
|
あなたはTAKT(AIエージェントオーケストレーションツール)の一部として動作しています。
|
||||||
|
|
||||||
|
## TAKTの仕組み
|
||||||
|
- **ピース**: 複数のムーブメントを組み合わせた処理フロー(実装→レビュー→修正など)
|
||||||
|
- **ムーブメント**: 個別のエージェント実行単位(あなたが今担当している部分)
|
||||||
|
- **あなたの役割**: ピース全体の中で、現在のムーブメントに割り当てられた作業を実行する
|
||||||
|
|
||||||
|
## 現在のコンテキスト
|
||||||
|
- ピース: {{pieceName}}
|
||||||
|
- 現在のムーブメント: {{currentMovement}}
|
||||||
|
- 処理フロー:
|
||||||
|
{{movementsList}}
|
||||||
|
- 現在の位置: {{currentPosition}}
|
||||||
|
|
||||||
|
Phase 1実行時、あなたはピース名・ムーブメント名・処理フロー全体の情報を受け取ります。前後のムーブメントとの連携を意識して作業してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
{{agentDefinition}}
|
{{agentDefinition}}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<!--
|
<!--
|
||||||
template: perform_phase1_message
|
template: perform_phase1_message
|
||||||
phase: 1 (main execution)
|
phase: 1 (main execution)
|
||||||
vars: workingDirectory, editRule, pieceStructure, iteration, movementIteration,
|
vars: workingDirectory, editRule, pieceName, pieceDescription, hasPieceDescription,
|
||||||
movement, hasReport, reportInfo, phaseNote, hasTaskSection, userRequest,
|
pieceStructure, iteration, movementIteration, movement, hasReport, reportInfo,
|
||||||
hasPreviousResponse, previousResponse, hasUserInputs, userInputs, instructions
|
phaseNote, hasTaskSection, userRequest, hasPreviousResponse, previousResponse,
|
||||||
|
hasUserInputs, userInputs, instructions
|
||||||
builder: InstructionBuilder
|
builder: InstructionBuilder
|
||||||
-->
|
-->
|
||||||
## 実行コンテキスト
|
## 実行コンテキスト
|
||||||
@ -16,7 +17,10 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
## Piece Context
|
## Piece Context
|
||||||
{{#if pieceStructure}}{{pieceStructure}}
|
{{#if pieceName}}- ピース: {{pieceName}}
|
||||||
|
{{/if}}{{#if hasPieceDescription}}- 説明: {{pieceDescription}}
|
||||||
|
|
||||||
|
{{/if}}{{#if pieceStructure}}{{pieceStructure}}
|
||||||
|
|
||||||
{{/if}}- Iteration: {{iteration}}(ピース全体)
|
{{/if}}- Iteration: {{iteration}}(ピース全体)
|
||||||
- Movement Iteration: {{movementIteration}}(このムーブメントの実行回数)
|
- Movement Iteration: {{movementIteration}}(このムーブメントの実行回数)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<!--
|
<!--
|
||||||
template: score_interactive_system_prompt
|
template: score_interactive_system_prompt
|
||||||
role: system prompt for interactive planning mode
|
role: system prompt for interactive planning mode
|
||||||
vars: pieceInfo, pieceName, pieceDescription
|
vars: (none)
|
||||||
caller: features/interactive
|
caller: features/interactive
|
||||||
-->
|
-->
|
||||||
あなたはTAKT(AIエージェントピースオーケストレーションツール)の対話モードを担当しています。
|
あなたはTAKT(AIエージェントピースオーケストレーションツール)の対話モードを担当しています。
|
||||||
@ -49,11 +49,8 @@
|
|||||||
- Read/Glob/Grep/Bash を勝手に使わない。ユーザーが明示的に「あなた」に調査を依頼した場合のみ使用
|
- Read/Glob/Grep/Bash を勝手に使わない。ユーザーが明示的に「あなた」に調査を依頼した場合のみ使用
|
||||||
- スラッシュコマンドに言及しない(存在を知らない前提)
|
- スラッシュコマンドに言及しない(存在を知らない前提)
|
||||||
- ユーザーが満足したら次工程に進む。次の指示はしない
|
- ユーザーが満足したら次工程に進む。次の指示はしない
|
||||||
{{#if pieceInfo}}
|
|
||||||
|
|
||||||
## あなたが作成する指示書の行き先
|
## 指示書の提示について
|
||||||
このタスク指示書は「{{pieceName}}」ピースに渡されます。
|
- 対話中は指示書を勝手に提示しない
|
||||||
ピースの内容: {{pieceDescription}}
|
- ユーザーから「指示書を見せて」「いまどんな感じの指示書?」などの要求があった場合のみ、現在の理解を指示書形式で提示
|
||||||
|
- 最終的な指示書はユーザーに確定される(これはシステムが自動処理)
|
||||||
指示書は、このピースが期待する形式で作成してください。
|
|
||||||
{{/if}}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user