agent 周りの抽象化
This commit is contained in:
parent
1df353148e
commit
e23cfa9a3b
@ -119,6 +119,50 @@ function processOrder(order) {
|
|||||||
- 独自の書き方より標準的な書き方を選ぶ
|
- 独自の書き方より標準的な書き方を選ぶ
|
||||||
- 不明なときはリサーチする。推測で実装しない
|
- 不明なときはリサーチする。推測で実装しない
|
||||||
|
|
||||||
|
### インターフェース設計
|
||||||
|
|
||||||
|
インターフェースは利用側の都合で設計する。実装側の内部構造を露出しない。
|
||||||
|
|
||||||
|
| 原則 | 基準 |
|
||||||
|
|------|------|
|
||||||
|
| 利用者視点 | 呼び出し側が必要としないものを押し付けない |
|
||||||
|
| 構成と実行の分離 | 「何を使うか」はセットアップ時に決定し、実行APIはシンプルに保つ |
|
||||||
|
| メソッド増殖の禁止 | 同じことをする複数メソッドは構成の違いで吸収する |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ メソッド増殖 — 構成の違いを呼び出し側に押し付けている
|
||||||
|
interface Provider {
|
||||||
|
call(name, prompt, options)
|
||||||
|
callCustom(name, prompt, systemPrompt, options)
|
||||||
|
callAgent(name, prompt, options)
|
||||||
|
callSkill(name, prompt, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 構成と実行の分離
|
||||||
|
interface Provider {
|
||||||
|
setup(config: Setup): Agent
|
||||||
|
}
|
||||||
|
interface Agent {
|
||||||
|
call(prompt, options): Promise<Response>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 抽象化の漏れ
|
||||||
|
|
||||||
|
特定実装が汎用層に現れたら抽象化が漏れている。汎用層はインターフェースだけを知り、分岐は実装側で吸収する。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 汎用層に特定実装のインポートと分岐
|
||||||
|
import { callSpecificImpl } from '../specific/index.js'
|
||||||
|
if (config.specificFlag) {
|
||||||
|
return callSpecificImpl(config.name, task, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 汎用層はインターフェースのみ。非対応は setup 時にエラー
|
||||||
|
const agent = provider.setup({ specificFlag: config.specificFlag })
|
||||||
|
return agent.call(task, options)
|
||||||
|
```
|
||||||
|
|
||||||
## 構造
|
## 構造
|
||||||
|
|
||||||
### 分割の基準
|
### 分割の基準
|
||||||
|
|||||||
@ -38,6 +38,8 @@
|
|||||||
- エラーの握りつぶし(空の catch)
|
- エラーの握りつぶし(空の catch)
|
||||||
- TODO コメント(Issue化されていないもの)
|
- TODO コメント(Issue化されていないもの)
|
||||||
- 3箇所以上の重複コード(DRY違反)
|
- 3箇所以上の重複コード(DRY違反)
|
||||||
|
- 同じことをするメソッドの増殖(構成の違いで吸収すべき)
|
||||||
|
- 特定実装の汎用層への漏洩(汎用層に特定実装のインポート・分岐がある)
|
||||||
|
|
||||||
### Warning(警告)
|
### Warning(警告)
|
||||||
|
|
||||||
|
|||||||
@ -105,18 +105,19 @@ function setupInputSequence(inputs: (string | null)[]): void {
|
|||||||
/** Create a mock provider that returns given responses */
|
/** Create a mock provider that returns given responses */
|
||||||
function setupMockProvider(responses: string[]): void {
|
function setupMockProvider(responses: string[]): void {
|
||||||
let callIndex = 0;
|
let callIndex = 0;
|
||||||
|
const mockCall = vi.fn(async () => {
|
||||||
|
const content = callIndex < responses.length ? responses[callIndex] : 'AI response';
|
||||||
|
callIndex++;
|
||||||
|
return {
|
||||||
|
persona: 'interactive',
|
||||||
|
status: 'done' as const,
|
||||||
|
content: content!,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
});
|
||||||
const mockProvider = {
|
const mockProvider = {
|
||||||
call: vi.fn(async () => {
|
setup: () => ({ call: mockCall }),
|
||||||
const content = callIndex < responses.length ? responses[callIndex] : 'AI response';
|
_call: mockCall,
|
||||||
callIndex++;
|
|
||||||
return {
|
|
||||||
persona: 'interactive',
|
|
||||||
status: 'done' as const,
|
|
||||||
content: content!,
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
callCustom: vi.fn(),
|
|
||||||
};
|
};
|
||||||
mockGetProvider.mockReturnValue(mockProvider);
|
mockGetProvider.mockReturnValue(mockProvider);
|
||||||
}
|
}
|
||||||
@ -162,9 +163,8 @@ describe('interactiveMode', () => {
|
|||||||
await interactiveMode('/project');
|
await interactiveMode('/project');
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||||
expect(mockProvider.call).toHaveBeenCalledWith(
|
expect(mockProvider._call).toHaveBeenCalledWith(
|
||||||
'interactive',
|
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cwd: '/project',
|
cwd: '/project',
|
||||||
@ -208,8 +208,8 @@ describe('interactiveMode', () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(result.action).toBe('execute');
|
expect(result.action).toBe('execute');
|
||||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||||
expect(mockProvider.call).toHaveBeenCalledTimes(2);
|
expect(mockProvider._call).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accumulate conversation history across multiple turns', async () => {
|
it('should accumulate conversation history across multiple turns', async () => {
|
||||||
@ -223,8 +223,8 @@ describe('interactiveMode', () => {
|
|||||||
// Then: task should be a summary and prompt should include full history
|
// Then: task should be a summary and prompt should include full history
|
||||||
expect(result.action).toBe('execute');
|
expect(result.action).toBe('execute');
|
||||||
expect(result.task).toBe('Summarized task.');
|
expect(result.task).toBe('Summarized task.');
|
||||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||||
const summaryPrompt = mockProvider.call.mock.calls[2]?.[1] as string;
|
const summaryPrompt = mockProvider._call.mock.calls[2]?.[0] as string;
|
||||||
expect(summaryPrompt).toContain('Conversation:');
|
expect(summaryPrompt).toContain('Conversation:');
|
||||||
expect(summaryPrompt).toContain('User: first message');
|
expect(summaryPrompt).toContain('User: first message');
|
||||||
expect(summaryPrompt).toContain('Assistant: response to first');
|
expect(summaryPrompt).toContain('Assistant: response to first');
|
||||||
@ -241,9 +241,9 @@ describe('interactiveMode', () => {
|
|||||||
await interactiveMode('/project');
|
await interactiveMode('/project');
|
||||||
|
|
||||||
// Then: each call receives only the current user input (session maintains context)
|
// Then: each call receives only the current user input (session maintains context)
|
||||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('first msg');
|
expect(mockProvider._call.mock.calls[0]?.[0]).toBe('first msg');
|
||||||
expect(mockProvider.call.mock.calls[1]?.[1]).toBe('second msg');
|
expect(mockProvider._call.mock.calls[1]?.[0]).toBe('second msg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process initialInput as first message before entering loop', async () => {
|
it('should process initialInput as first message before entering loop', async () => {
|
||||||
@ -255,9 +255,9 @@ describe('interactiveMode', () => {
|
|||||||
const result = await interactiveMode('/project', 'a');
|
const result = await interactiveMode('/project', 'a');
|
||||||
|
|
||||||
// Then: AI should have been called with initialInput
|
// Then: AI should have been called with initialInput
|
||||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||||
expect(mockProvider.call).toHaveBeenCalledTimes(2);
|
expect(mockProvider._call).toHaveBeenCalledTimes(2);
|
||||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
|
expect(mockProvider._call.mock.calls[0]?.[0]).toBe('a');
|
||||||
|
|
||||||
// /go should work because initialInput already started conversation
|
// /go should work because initialInput already started conversation
|
||||||
expect(result.action).toBe('execute');
|
expect(result.action).toBe('execute');
|
||||||
@ -273,10 +273,10 @@ describe('interactiveMode', () => {
|
|||||||
const result = await interactiveMode('/project', 'a');
|
const result = await interactiveMode('/project', 'a');
|
||||||
|
|
||||||
// Then: each call receives only its own input (session handles history)
|
// Then: each call receives only its own input (session handles history)
|
||||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||||
expect(mockProvider.call).toHaveBeenCalledTimes(3);
|
expect(mockProvider._call).toHaveBeenCalledTimes(3);
|
||||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
|
expect(mockProvider._call.mock.calls[0]?.[0]).toBe('a');
|
||||||
expect(mockProvider.call.mock.calls[1]?.[1]).toBe('fix the login page');
|
expect(mockProvider._call.mock.calls[1]?.[0]).toBe('fix the login page');
|
||||||
|
|
||||||
// Task still contains all history for downstream use
|
// Task still contains all history for downstream use
|
||||||
expect(result.action).toBe('execute');
|
expect(result.action).toBe('execute');
|
||||||
@ -332,7 +332,7 @@ describe('interactiveMode', () => {
|
|||||||
|
|
||||||
// Then: provider should NOT have been called (no summary needed)
|
// Then: provider should NOT have been called (no summary needed)
|
||||||
const mockProvider = mockGetProvider.mock.results[0]?.value as { call: ReturnType<typeof vi.fn> };
|
const mockProvider = mockGetProvider.mock.results[0]?.value as { call: ReturnType<typeof vi.fn> };
|
||||||
expect(mockProvider.call).not.toHaveBeenCalled();
|
expect(mockProvider._call).not.toHaveBeenCalled();
|
||||||
expect(result.action).toBe('execute');
|
expect(result.action).toBe('execute');
|
||||||
expect(result.task).toBe('quick task');
|
expect(result.task).toBe('quick task');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,8 +31,7 @@ const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
|||||||
|
|
||||||
const mockProviderCall = vi.fn();
|
const mockProviderCall = vi.fn();
|
||||||
const mockProvider = {
|
const mockProvider = {
|
||||||
call: mockProviderCall,
|
setup: () => ({ call: mockProviderCall }),
|
||||||
callCustom: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -65,7 +64,6 @@ describe('summarizeTaskName', () => {
|
|||||||
expect(result).toBe('add-auth');
|
expect(result).toBe('add-auth');
|
||||||
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
||||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||||
'summarizer',
|
|
||||||
'long task name for testing',
|
'long task name for testing',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cwd: '/project',
|
cwd: '/project',
|
||||||
@ -154,7 +152,6 @@ describe('summarizeTaskName', () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||||
'summarizer',
|
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
model: 'sonnet',
|
model: 'sonnet',
|
||||||
@ -185,7 +182,6 @@ describe('summarizeTaskName', () => {
|
|||||||
// Then
|
// Then
|
||||||
expect(mockGetProvider).toHaveBeenCalledWith('codex');
|
expect(mockGetProvider).toHaveBeenCalledWith('codex');
|
||||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||||
'summarizer',
|
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
|
|||||||
@ -4,11 +4,6 @@
|
|||||||
|
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { basename, dirname } from 'node:path';
|
import { basename, dirname } from 'node:path';
|
||||||
import {
|
|
||||||
callClaudeAgent,
|
|
||||||
callClaudeSkill,
|
|
||||||
type ClaudeCallOptions,
|
|
||||||
} from '../infra/claude/index.js';
|
|
||||||
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
||||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||||
@ -46,9 +41,13 @@ export class AgentRunner {
|
|||||||
return 'claude';
|
return 'claude';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve model from options, agent config, global config */
|
/**
|
||||||
|
* Resolve model from options, agent config, global config.
|
||||||
|
* Global config model is only used when its provider matches the resolved provider,
|
||||||
|
* preventing cross-provider model mismatches (e.g., 'opus' sent to Codex).
|
||||||
|
*/
|
||||||
private static resolveModel(
|
private static resolveModel(
|
||||||
cwd: string,
|
resolvedProvider: ProviderType,
|
||||||
options?: RunAgentOptions,
|
options?: RunAgentOptions,
|
||||||
agentConfig?: CustomAgentConfig,
|
agentConfig?: CustomAgentConfig,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@ -56,7 +55,10 @@ export class AgentRunner {
|
|||||||
if (agentConfig?.model) return agentConfig.model;
|
if (agentConfig?.model) return agentConfig.model;
|
||||||
try {
|
try {
|
||||||
const globalConfig = loadGlobalConfig();
|
const globalConfig = loadGlobalConfig();
|
||||||
if (globalConfig.model) return globalConfig.model;
|
if (globalConfig.model) {
|
||||||
|
const globalProvider = globalConfig.provider ?? 'claude';
|
||||||
|
if (globalProvider === resolvedProvider) return globalConfig.model;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore missing global config
|
// Ignore missing global config
|
||||||
}
|
}
|
||||||
@ -90,88 +92,45 @@ export class AgentRunner {
|
|||||||
return `${dir}/${name}`;
|
return `${dir}/${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Build ProviderCallOptions from RunAgentOptions */
|
||||||
|
private static buildCallOptions(
|
||||||
|
resolvedProvider: ProviderType,
|
||||||
|
options: RunAgentOptions,
|
||||||
|
agentConfig?: CustomAgentConfig,
|
||||||
|
): ProviderCallOptions {
|
||||||
|
return {
|
||||||
|
cwd: options.cwd,
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
allowedTools: options.allowedTools ?? agentConfig?.allowedTools,
|
||||||
|
maxTurns: options.maxTurns,
|
||||||
|
model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig),
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
|
onStream: options.onStream,
|
||||||
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Run a custom agent */
|
/** Run a custom agent */
|
||||||
async runCustom(
|
async runCustom(
|
||||||
agentConfig: CustomAgentConfig,
|
agentConfig: CustomAgentConfig,
|
||||||
task: string,
|
task: string,
|
||||||
options: RunAgentOptions,
|
options: RunAgentOptions,
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const allowedTools = options.allowedTools ?? agentConfig.allowedTools;
|
|
||||||
|
|
||||||
// If agent references a Claude Code agent
|
|
||||||
if (agentConfig.claudeAgent) {
|
|
||||||
const callOptions: ClaudeCallOptions = {
|
|
||||||
cwd: options.cwd,
|
|
||||||
sessionId: options.sessionId,
|
|
||||||
allowedTools,
|
|
||||||
maxTurns: options.maxTurns,
|
|
||||||
model: AgentRunner.resolveModel(options.cwd, options, agentConfig),
|
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
|
||||||
bypassPermissions: options.bypassPermissions,
|
|
||||||
};
|
|
||||||
return callClaudeAgent(agentConfig.claudeAgent, task, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If agent references a Claude Code skill
|
|
||||||
if (agentConfig.claudeSkill) {
|
|
||||||
const callOptions: ClaudeCallOptions = {
|
|
||||||
cwd: options.cwd,
|
|
||||||
sessionId: options.sessionId,
|
|
||||||
allowedTools,
|
|
||||||
maxTurns: options.maxTurns,
|
|
||||||
model: AgentRunner.resolveModel(options.cwd, options, agentConfig),
|
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
|
||||||
bypassPermissions: options.bypassPermissions,
|
|
||||||
};
|
|
||||||
return callClaudeSkill(agentConfig.claudeSkill, task, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom agent with prompt
|
|
||||||
const systemPrompt = loadAgentPrompt(agentConfig);
|
|
||||||
|
|
||||||
const providerType = AgentRunner.resolveProvider(options.cwd, options, agentConfig);
|
const providerType = AgentRunner.resolveProvider(options.cwd, options, agentConfig);
|
||||||
const provider = getProvider(providerType);
|
const provider = getProvider(providerType);
|
||||||
|
|
||||||
const callOptions: ProviderCallOptions = {
|
const agent = provider.setup({
|
||||||
cwd: options.cwd,
|
name: agentConfig.name,
|
||||||
sessionId: options.sessionId,
|
systemPrompt: agentConfig.claudeAgent || agentConfig.claudeSkill
|
||||||
allowedTools,
|
? undefined
|
||||||
maxTurns: options.maxTurns,
|
: loadAgentPrompt(agentConfig),
|
||||||
model: AgentRunner.resolveModel(options.cwd, options, agentConfig),
|
claudeAgent: agentConfig.claudeAgent,
|
||||||
permissionMode: options.permissionMode,
|
claudeSkill: agentConfig.claudeSkill,
|
||||||
onStream: options.onStream,
|
});
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
|
||||||
bypassPermissions: options.bypassPermissions,
|
|
||||||
};
|
|
||||||
|
|
||||||
return provider.callCustom(agentConfig.name, task, systemPrompt, callOptions);
|
return agent.call(task, AgentRunner.buildCallOptions(providerType, options, agentConfig));
|
||||||
}
|
|
||||||
|
|
||||||
/** Build ProviderCallOptions from RunAgentOptions with optional systemPrompt override */
|
|
||||||
private static buildProviderCallOptions(
|
|
||||||
options: RunAgentOptions,
|
|
||||||
systemPrompt?: string,
|
|
||||||
): ProviderCallOptions {
|
|
||||||
return {
|
|
||||||
cwd: options.cwd,
|
|
||||||
sessionId: options.sessionId,
|
|
||||||
allowedTools: options.allowedTools,
|
|
||||||
maxTurns: options.maxTurns,
|
|
||||||
model: AgentRunner.resolveModel(options.cwd, options),
|
|
||||||
systemPrompt,
|
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
|
||||||
bypassPermissions: options.bypassPermissions,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
||||||
@ -191,6 +150,10 @@ export class AgentRunner {
|
|||||||
permissionMode: options.permissionMode,
|
permissionMode: options.permissionMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||||
|
const provider = getProvider(providerType);
|
||||||
|
const callOptions = AgentRunner.buildCallOptions(providerType, options);
|
||||||
|
|
||||||
// 1. If personaPath is provided (resolved file exists), load prompt from file
|
// 1. If personaPath is provided (resolved file exists), load prompt from file
|
||||||
// and wrap it through the perform_agent_system_prompt template
|
// and wrap it through the perform_agent_system_prompt template
|
||||||
if (options.personaPath) {
|
if (options.personaPath) {
|
||||||
@ -198,7 +161,6 @@ export class AgentRunner {
|
|||||||
const language = options.language ?? 'en';
|
const language = options.language ?? 'en';
|
||||||
const templateVars: Record<string, string> = { agentDefinition };
|
const templateVars: Record<string, string> = { agentDefinition };
|
||||||
|
|
||||||
// Add piece meta information if available
|
|
||||||
if (options.pieceMeta) {
|
if (options.pieceMeta) {
|
||||||
templateVars.pieceName = options.pieceMeta.pieceName;
|
templateVars.pieceName = options.pieceMeta.pieceName;
|
||||||
templateVars.pieceDescription = options.pieceMeta.pieceDescription ?? '';
|
templateVars.pieceDescription = options.pieceMeta.pieceDescription ?? '';
|
||||||
@ -210,9 +172,8 @@ export class AgentRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const systemPrompt = loadTemplate('perform_agent_system_prompt', language, templateVars);
|
const systemPrompt = loadTemplate('perform_agent_system_prompt', language, templateVars);
|
||||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
const agent = provider.setup({ name: personaName, systemPrompt });
|
||||||
const provider = getProvider(providerType);
|
return agent.call(task, callOptions);
|
||||||
return provider.call(personaName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If personaSpec is provided but no personaPath (file not found), try custom agent first,
|
// 2. If personaSpec is provided but no personaPath (file not found), try custom agent first,
|
||||||
@ -224,16 +185,13 @@ export class AgentRunner {
|
|||||||
return this.runCustom(agentConfig, task, options);
|
return this.runCustom(agentConfig, task, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use personaSpec string as inline system prompt
|
const agent = provider.setup({ name: personaName, systemPrompt: personaSpec });
|
||||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
return agent.call(task, callOptions);
|
||||||
const provider = getProvider(providerType);
|
|
||||||
return provider.call(personaName, task, AgentRunner.buildProviderCallOptions(options, personaSpec));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. No persona specified — run with instruction_template only (no system prompt)
|
// 3. No persona specified — run with instruction_template only (no system prompt)
|
||||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
const agent = provider.setup({ name: personaName });
|
||||||
const provider = getProvider(providerType);
|
return agent.call(task, callOptions);
|
||||||
return provider.call(personaName, task, AgentRunner.buildProviderCallOptions(options));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -221,11 +221,11 @@ async function callAI(
|
|||||||
display: StreamDisplay,
|
display: StreamDisplay,
|
||||||
systemPrompt: string,
|
systemPrompt: string,
|
||||||
): Promise<CallAIResult> {
|
): Promise<CallAIResult> {
|
||||||
const response = await provider.call('interactive', prompt, {
|
const agent = provider.setup({ name: 'interactive', systemPrompt });
|
||||||
|
const response = await agent.call(prompt, {
|
||||||
cwd,
|
cwd,
|
||||||
model,
|
model,
|
||||||
sessionId,
|
sessionId,
|
||||||
systemPrompt,
|
|
||||||
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
|
||||||
onStream: display.createHandler(),
|
onStream: display.createHandler(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,47 +2,57 @@
|
|||||||
* Claude provider implementation
|
* Claude provider implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/index.js';
|
import { callClaude, callClaudeCustom, callClaudeAgent, callClaudeSkill, type ClaudeCallOptions } from '../claude/index.js';
|
||||||
import { resolveAnthropicApiKey } from '../config/index.js';
|
import { resolveAnthropicApiKey } from '../config/index.js';
|
||||||
import type { AgentResponse } from '../../core/models/index.js';
|
import type { AgentResponse } from '../../core/models/index.js';
|
||||||
import type { Provider, ProviderCallOptions } from './types.js';
|
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
||||||
|
|
||||||
/** Claude provider - wraps existing Claude client */
|
function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
||||||
|
return {
|
||||||
|
cwd: options.cwd,
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
allowedTools: options.allowedTools,
|
||||||
|
model: options.model,
|
||||||
|
maxTurns: options.maxTurns,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
|
onStream: options.onStream,
|
||||||
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Claude provider — delegates to Claude Code SDK */
|
||||||
export class ClaudeProvider implements Provider {
|
export class ClaudeProvider implements Provider {
|
||||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
setup(config: AgentSetup): ProviderAgent {
|
||||||
const callOptions: ClaudeCallOptions = {
|
if (config.claudeAgent) {
|
||||||
cwd: options.cwd,
|
const agentName = config.claudeAgent;
|
||||||
sessionId: options.sessionId,
|
return {
|
||||||
allowedTools: options.allowedTools,
|
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||||
model: options.model,
|
callClaudeAgent(agentName, prompt, toClaudeOptions(options)),
|
||||||
maxTurns: options.maxTurns,
|
};
|
||||||
systemPrompt: options.systemPrompt,
|
}
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
if (config.claudeSkill) {
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
const skillName = config.claudeSkill;
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
return {
|
||||||
bypassPermissions: options.bypassPermissions,
|
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||||
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
callClaudeSkill(skillName, prompt, toClaudeOptions(options)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, systemPrompt } = config;
|
||||||
|
if (systemPrompt) {
|
||||||
|
return {
|
||||||
|
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||||
|
callClaudeCustom(name, prompt, systemPrompt, toClaudeOptions(options)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||||
|
callClaude(name, prompt, toClaudeOptions(options)),
|
||||||
};
|
};
|
||||||
|
|
||||||
return callClaude(agentName, prompt, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
|
||||||
const callOptions: ClaudeCallOptions = {
|
|
||||||
cwd: options.cwd,
|
|
||||||
sessionId: options.sessionId,
|
|
||||||
allowedTools: options.allowedTools,
|
|
||||||
model: options.model,
|
|
||||||
maxTurns: options.maxTurns,
|
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
|
||||||
bypassPermissions: options.bypassPermissions,
|
|
||||||
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return callClaudeCustom(agentName, prompt, systemPrompt, callOptions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { execFileSync } from 'node:child_process';
|
|||||||
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js';
|
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js';
|
||||||
import { resolveOpenaiApiKey } from '../config/index.js';
|
import { resolveOpenaiApiKey } from '../config/index.js';
|
||||||
import type { AgentResponse } from '../../core/models/index.js';
|
import type { AgentResponse } from '../../core/models/index.js';
|
||||||
import type { Provider, ProviderCallOptions } from './types.js';
|
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
||||||
|
|
||||||
const NOT_GIT_REPO_MESSAGE =
|
const NOT_GIT_REPO_MESSAGE =
|
||||||
'Codex をご利用の場合 Git 管理下のディレクトリでのみ動作します。';
|
'Codex をご利用の場合 Git 管理下のディレクトリでのみ動作します。';
|
||||||
@ -24,50 +24,51 @@ function isInsideGitRepo(cwd: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Codex provider - wraps existing Codex client */
|
function toCodexOptions(options: ProviderCallOptions): CodexCallOptions {
|
||||||
|
return {
|
||||||
|
cwd: options.cwd,
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
model: options.model,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
|
onStream: options.onStream,
|
||||||
|
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockedResponse(agentName: string): AgentResponse {
|
||||||
|
return {
|
||||||
|
persona: agentName,
|
||||||
|
status: 'blocked',
|
||||||
|
content: NOT_GIT_REPO_MESSAGE,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Codex provider — delegates to OpenAI Codex SDK */
|
||||||
export class CodexProvider implements Provider {
|
export class CodexProvider implements Provider {
|
||||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
setup(config: AgentSetup): ProviderAgent {
|
||||||
if (!isInsideGitRepo(options.cwd)) {
|
if (config.claudeAgent) {
|
||||||
|
throw new Error('Claude Code agent calls are not supported by the Codex provider');
|
||||||
|
}
|
||||||
|
if (config.claudeSkill) {
|
||||||
|
throw new Error('Claude Code skill calls are not supported by the Codex provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, systemPrompt } = config;
|
||||||
|
if (systemPrompt) {
|
||||||
return {
|
return {
|
||||||
persona: agentName,
|
call: async (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> => {
|
||||||
status: 'blocked',
|
if (!isInsideGitRepo(options.cwd)) return blockedResponse(name);
|
||||||
content: NOT_GIT_REPO_MESSAGE,
|
return callCodexCustom(name, prompt, systemPrompt, toCodexOptions(options));
|
||||||
timestamp: new Date(),
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const callOptions: CodexCallOptions = {
|
return {
|
||||||
cwd: options.cwd,
|
call: async (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> => {
|
||||||
sessionId: options.sessionId,
|
if (!isInsideGitRepo(options.cwd)) return blockedResponse(name);
|
||||||
model: options.model,
|
return callCodex(name, prompt, toCodexOptions(options));
|
||||||
systemPrompt: options.systemPrompt,
|
},
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
|
||||||
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return callCodex(agentName, prompt, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
|
||||||
if (!isInsideGitRepo(options.cwd)) {
|
|
||||||
return {
|
|
||||||
persona: agentName,
|
|
||||||
status: 'blocked',
|
|
||||||
content: NOT_GIT_REPO_MESSAGE,
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const callOptions: CodexCallOptions = {
|
|
||||||
cwd: options.cwd,
|
|
||||||
sessionId: options.sessionId,
|
|
||||||
model: options.model,
|
|
||||||
permissionMode: options.permissionMode,
|
|
||||||
onStream: options.onStream,
|
|
||||||
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return callCodexCustom(agentName, prompt, systemPrompt, callOptions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { CodexProvider } from './codex.js';
|
|||||||
import { MockProvider } from './mock.js';
|
import { MockProvider } from './mock.js';
|
||||||
import type { Provider, ProviderType } from './types.js';
|
import type { Provider, ProviderType } from './types.js';
|
||||||
|
|
||||||
export type { ProviderCallOptions, Provider, ProviderType } from './types.js';
|
export type { AgentSetup, ProviderCallOptions, ProviderAgent, Provider, ProviderType } from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry for agent providers.
|
* Registry for agent providers.
|
||||||
|
|||||||
@ -4,27 +4,37 @@
|
|||||||
|
|
||||||
import { callMock, callMockCustom, type MockCallOptions } from '../mock/index.js';
|
import { callMock, callMockCustom, type MockCallOptions } from '../mock/index.js';
|
||||||
import type { AgentResponse } from '../../core/models/index.js';
|
import type { AgentResponse } from '../../core/models/index.js';
|
||||||
import type { Provider, ProviderCallOptions } from './types.js';
|
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
||||||
|
|
||||||
/** Mock provider - wraps existing Mock client */
|
function toMockOptions(options: ProviderCallOptions): MockCallOptions {
|
||||||
|
return {
|
||||||
|
cwd: options.cwd,
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
onStream: options.onStream,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mock provider — deterministic responses for testing */
|
||||||
export class MockProvider implements Provider {
|
export class MockProvider implements Provider {
|
||||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
setup(config: AgentSetup): ProviderAgent {
|
||||||
const callOptions: MockCallOptions = {
|
if (config.claudeAgent) {
|
||||||
cwd: options.cwd,
|
throw new Error('Claude Code agent calls are not supported by the Mock provider');
|
||||||
sessionId: options.sessionId,
|
}
|
||||||
onStream: options.onStream,
|
if (config.claudeSkill) {
|
||||||
|
throw new Error('Claude Code skill calls are not supported by the Mock provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, systemPrompt } = config;
|
||||||
|
if (systemPrompt) {
|
||||||
|
return {
|
||||||
|
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||||
|
callMockCustom(name, prompt, systemPrompt, toMockOptions(options)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||||
|
callMock(name, prompt, toMockOptions(options)),
|
||||||
};
|
};
|
||||||
|
|
||||||
return callMock(agentName, prompt, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async callCustom(agentName: string, prompt: string, _systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
|
||||||
const callOptions: MockCallOptions = {
|
|
||||||
cwd: options.cwd,
|
|
||||||
sessionId: options.sessionId,
|
|
||||||
onStream: options.onStream,
|
|
||||||
};
|
|
||||||
|
|
||||||
return callMockCustom(agentName, prompt, _systemPrompt, callOptions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,23 @@
|
|||||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/index.js';
|
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/index.js';
|
||||||
import type { AgentResponse, PermissionMode } from '../../core/models/index.js';
|
import type { AgentResponse, PermissionMode } from '../../core/models/index.js';
|
||||||
|
|
||||||
/** Common options for all providers */
|
/** Agent setup configuration — determines HOW the provider invokes the agent */
|
||||||
|
export interface AgentSetup {
|
||||||
|
/** Display name for this agent */
|
||||||
|
name: string;
|
||||||
|
/** System prompt for the agent (persona content, inline prompt, etc.) */
|
||||||
|
systemPrompt?: string;
|
||||||
|
/** Delegate to a Claude Code agent by name (Claude provider only) */
|
||||||
|
claudeAgent?: string;
|
||||||
|
/** Delegate to a Claude Code skill by name (Claude provider only) */
|
||||||
|
claudeSkill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runtime options passed at call time */
|
||||||
export interface ProviderCallOptions {
|
export interface ProviderCallOptions {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
systemPrompt?: string;
|
|
||||||
allowedTools?: string[];
|
allowedTools?: string[];
|
||||||
/** Maximum number of agentic turns */
|
/** Maximum number of agentic turns */
|
||||||
maxTurns?: number;
|
maxTurns?: number;
|
||||||
@ -26,13 +37,14 @@ export interface ProviderCallOptions {
|
|||||||
openaiApiKey?: string;
|
openaiApiKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider interface - all providers must implement this */
|
/** A configured agent ready to be called */
|
||||||
export interface Provider {
|
export interface ProviderAgent {
|
||||||
/** Call the provider with a prompt (using systemPrompt from options if provided) */
|
call(prompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
|
||||||
call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
|
}
|
||||||
|
|
||||||
/** Call the provider with explicit system prompt */
|
/** Provider interface — creates configured agents from setup */
|
||||||
callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
|
export interface Provider {
|
||||||
|
setup(config: AgentSetup): ProviderAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider type */
|
/** Provider type */
|
||||||
|
|||||||
@ -66,15 +66,15 @@ export class TaskSummarizer {
|
|||||||
const model = options.model ?? globalConfig.model;
|
const model = options.model ?? globalConfig.model;
|
||||||
|
|
||||||
const provider = getProvider(providerType);
|
const provider = getProvider(providerType);
|
||||||
const callOptions: SummarizeOptions & { systemPrompt: string; allowedTools: [] } = {
|
const agent = provider.setup({
|
||||||
cwd: options.cwd,
|
name: 'summarizer',
|
||||||
systemPrompt: loadTemplate('score_slug_system_prompt', 'en'),
|
systemPrompt: loadTemplate('score_slug_system_prompt', 'en'),
|
||||||
|
});
|
||||||
|
const response = await agent.call(taskName, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
model,
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
};
|
});
|
||||||
if (model) {
|
|
||||||
callOptions.model = model;
|
|
||||||
}
|
|
||||||
const response = await provider.call('summarizer', taskName, callOptions);
|
|
||||||
|
|
||||||
const slug = sanitizeSlug(response.content);
|
const slug = sanitizeSlug(response.content);
|
||||||
log.info('Task name summarized', { original: taskName, slug });
|
log.info('Task name summarized', { original: taskName, slug });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user