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)
|
||||
- TODO コメント(Issue化されていないもの)
|
||||
- 3箇所以上の重複コード(DRY違反)
|
||||
- 同じことをするメソッドの増殖(構成の違いで吸収すべき)
|
||||
- 特定実装の汎用層への漏洩(汎用層に特定実装のインポート・分岐がある)
|
||||
|
||||
### Warning(警告)
|
||||
|
||||
|
||||
@ -105,18 +105,19 @@ function setupInputSequence(inputs: (string | null)[]): void {
|
||||
/** Create a mock provider that returns given responses */
|
||||
function setupMockProvider(responses: string[]): void {
|
||||
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 = {
|
||||
call: 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(),
|
||||
};
|
||||
}),
|
||||
callCustom: vi.fn(),
|
||||
setup: () => ({ call: mockCall }),
|
||||
_call: mockCall,
|
||||
};
|
||||
mockGetProvider.mockReturnValue(mockProvider);
|
||||
}
|
||||
@ -162,9 +163,8 @@ describe('interactiveMode', () => {
|
||||
await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider.call).toHaveBeenCalledWith(
|
||||
'interactive',
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
cwd: '/project',
|
||||
@ -208,8 +208,8 @@ describe('interactiveMode', () => {
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider.call).toHaveBeenCalledTimes(2);
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
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
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Summarized task.');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
const summaryPrompt = mockProvider.call.mock.calls[2]?.[1] as string;
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
const summaryPrompt = mockProvider._call.mock.calls[2]?.[0] as string;
|
||||
expect(summaryPrompt).toContain('Conversation:');
|
||||
expect(summaryPrompt).toContain('User: first message');
|
||||
expect(summaryPrompt).toContain('Assistant: response to first');
|
||||
@ -241,9 +241,9 @@ describe('interactiveMode', () => {
|
||||
await interactiveMode('/project');
|
||||
|
||||
// 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> };
|
||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('first msg');
|
||||
expect(mockProvider.call.mock.calls[1]?.[1]).toBe('second msg');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call.mock.calls[0]?.[0]).toBe('first msg');
|
||||
expect(mockProvider._call.mock.calls[1]?.[0]).toBe('second msg');
|
||||
});
|
||||
|
||||
it('should process initialInput as first message before entering loop', async () => {
|
||||
@ -255,9 +255,9 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project', 'a');
|
||||
|
||||
// Then: AI should have been called with initialInput
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider.call).toHaveBeenCalledTimes(2);
|
||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledTimes(2);
|
||||
expect(mockProvider._call.mock.calls[0]?.[0]).toBe('a');
|
||||
|
||||
// /go should work because initialInput already started conversation
|
||||
expect(result.action).toBe('execute');
|
||||
@ -273,10 +273,10 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project', 'a');
|
||||
|
||||
// Then: each call receives only its own input (session handles history)
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider.call).toHaveBeenCalledTimes(3);
|
||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
|
||||
expect(mockProvider.call.mock.calls[1]?.[1]).toBe('fix the login page');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledTimes(3);
|
||||
expect(mockProvider._call.mock.calls[0]?.[0]).toBe('a');
|
||||
expect(mockProvider._call.mock.calls[1]?.[0]).toBe('fix the login page');
|
||||
|
||||
// Task still contains all history for downstream use
|
||||
expect(result.action).toBe('execute');
|
||||
@ -332,7 +332,7 @@ describe('interactiveMode', () => {
|
||||
|
||||
// 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(mockProvider._call).not.toHaveBeenCalled();
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('quick task');
|
||||
});
|
||||
|
||||
@ -31,8 +31,7 @@ const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
||||
|
||||
const mockProviderCall = vi.fn();
|
||||
const mockProvider = {
|
||||
call: mockProviderCall,
|
||||
callCustom: vi.fn(),
|
||||
setup: () => ({ call: mockProviderCall }),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@ -65,7 +64,6 @@ describe('summarizeTaskName', () => {
|
||||
expect(result).toBe('add-auth');
|
||||
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||
'summarizer',
|
||||
'long task name for testing',
|
||||
expect.objectContaining({
|
||||
cwd: '/project',
|
||||
@ -154,7 +152,6 @@ describe('summarizeTaskName', () => {
|
||||
|
||||
// Then
|
||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||
'summarizer',
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
model: 'sonnet',
|
||||
@ -185,7 +182,6 @@ describe('summarizeTaskName', () => {
|
||||
// Then
|
||||
expect(mockGetProvider).toHaveBeenCalledWith('codex');
|
||||
expect(mockProviderCall).toHaveBeenCalledWith(
|
||||
'summarizer',
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
model: 'gpt-4',
|
||||
|
||||
@ -4,11 +4,6 @@
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
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 { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||
@ -46,9 +41,13 @@ export class AgentRunner {
|
||||
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(
|
||||
cwd: string,
|
||||
resolvedProvider: ProviderType,
|
||||
options?: RunAgentOptions,
|
||||
agentConfig?: CustomAgentConfig,
|
||||
): string | undefined {
|
||||
@ -56,7 +55,10 @@ export class AgentRunner {
|
||||
if (agentConfig?.model) return agentConfig.model;
|
||||
try {
|
||||
const globalConfig = loadGlobalConfig();
|
||||
if (globalConfig.model) return globalConfig.model;
|
||||
if (globalConfig.model) {
|
||||
const globalProvider = globalConfig.provider ?? 'claude';
|
||||
if (globalProvider === resolvedProvider) return globalConfig.model;
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing global config
|
||||
}
|
||||
@ -90,88 +92,45 @@ export class AgentRunner {
|
||||
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 */
|
||||
async runCustom(
|
||||
agentConfig: CustomAgentConfig,
|
||||
task: string,
|
||||
options: RunAgentOptions,
|
||||
): 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 provider = getProvider(providerType);
|
||||
|
||||
const callOptions: ProviderCallOptions = {
|
||||
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,
|
||||
};
|
||||
const agent = provider.setup({
|
||||
name: agentConfig.name,
|
||||
systemPrompt: agentConfig.claudeAgent || agentConfig.claudeSkill
|
||||
? undefined
|
||||
: loadAgentPrompt(agentConfig),
|
||||
claudeAgent: agentConfig.claudeAgent,
|
||||
claudeSkill: agentConfig.claudeSkill,
|
||||
});
|
||||
|
||||
return provider.callCustom(agentConfig.name, task, systemPrompt, callOptions);
|
||||
}
|
||||
|
||||
/** 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,
|
||||
};
|
||||
return agent.call(task, AgentRunner.buildCallOptions(providerType, options, agentConfig));
|
||||
}
|
||||
|
||||
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
||||
@ -191,6 +150,10 @@ export class AgentRunner {
|
||||
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
|
||||
// and wrap it through the perform_agent_system_prompt template
|
||||
if (options.personaPath) {
|
||||
@ -198,7 +161,6 @@ export class AgentRunner {
|
||||
const language = options.language ?? 'en';
|
||||
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 ?? '';
|
||||
@ -210,9 +172,8 @@ export class AgentRunner {
|
||||
}
|
||||
|
||||
const systemPrompt = loadTemplate('perform_agent_system_prompt', language, templateVars);
|
||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||
const provider = getProvider(providerType);
|
||||
return provider.call(personaName, task, AgentRunner.buildProviderCallOptions(options, systemPrompt));
|
||||
const agent = provider.setup({ name: personaName, systemPrompt });
|
||||
return agent.call(task, callOptions);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Use personaSpec string as inline system prompt
|
||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||
const provider = getProvider(providerType);
|
||||
return provider.call(personaName, task, AgentRunner.buildProviderCallOptions(options, personaSpec));
|
||||
const agent = provider.setup({ name: personaName, systemPrompt: personaSpec });
|
||||
return agent.call(task, callOptions);
|
||||
}
|
||||
|
||||
// 3. No persona specified — run with instruction_template only (no system prompt)
|
||||
const providerType = AgentRunner.resolveProvider(options.cwd, options);
|
||||
const provider = getProvider(providerType);
|
||||
return provider.call(personaName, task, AgentRunner.buildProviderCallOptions(options));
|
||||
const agent = provider.setup({ name: personaName });
|
||||
return agent.call(task, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -221,11 +221,11 @@ async function callAI(
|
||||
display: StreamDisplay,
|
||||
systemPrompt: string,
|
||||
): Promise<CallAIResult> {
|
||||
const response = await provider.call('interactive', prompt, {
|
||||
const agent = provider.setup({ name: 'interactive', systemPrompt });
|
||||
const response = await agent.call(prompt, {
|
||||
cwd,
|
||||
model,
|
||||
sessionId,
|
||||
systemPrompt,
|
||||
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
|
||||
onStream: display.createHandler(),
|
||||
});
|
||||
|
||||
@ -2,47 +2,57 @@
|
||||
* 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 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 {
|
||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
||||
const callOptions: ClaudeCallOptions = {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
allowedTools: options.allowedTools,
|
||||
model: options.model,
|
||||
maxTurns: options.maxTurns,
|
||||
systemPrompt: options.systemPrompt,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
onPermissionRequest: options.onPermissionRequest,
|
||||
onAskUserQuestion: options.onAskUserQuestion,
|
||||
bypassPermissions: options.bypassPermissions,
|
||||
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
||||
setup(config: AgentSetup): ProviderAgent {
|
||||
if (config.claudeAgent) {
|
||||
const agentName = config.claudeAgent;
|
||||
return {
|
||||
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||
callClaudeAgent(agentName, prompt, toClaudeOptions(options)),
|
||||
};
|
||||
}
|
||||
|
||||
if (config.claudeSkill) {
|
||||
const skillName = config.claudeSkill;
|
||||
return {
|
||||
call: (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> =>
|
||||
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 { resolveOpenaiApiKey } from '../config/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 =
|
||||
'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 {
|
||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
||||
if (!isInsideGitRepo(options.cwd)) {
|
||||
setup(config: AgentSetup): ProviderAgent {
|
||||
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 {
|
||||
persona: agentName,
|
||||
status: 'blocked',
|
||||
content: NOT_GIT_REPO_MESSAGE,
|
||||
timestamp: new Date(),
|
||||
call: async (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> => {
|
||||
if (!isInsideGitRepo(options.cwd)) return blockedResponse(name);
|
||||
return callCodexCustom(name, prompt, systemPrompt, toCodexOptions(options));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const callOptions: CodexCallOptions = {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
model: options.model,
|
||||
systemPrompt: options.systemPrompt,
|
||||
permissionMode: options.permissionMode,
|
||||
onStream: options.onStream,
|
||||
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||
return {
|
||||
call: async (prompt: string, options: ProviderCallOptions): Promise<AgentResponse> => {
|
||||
if (!isInsideGitRepo(options.cwd)) return blockedResponse(name);
|
||||
return callCodex(name, prompt, toCodexOptions(options));
|
||||
},
|
||||
};
|
||||
|
||||
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 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.
|
||||
|
||||
@ -4,27 +4,37 @@
|
||||
|
||||
import { callMock, callMockCustom, type MockCallOptions } from '../mock/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 {
|
||||
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
|
||||
const callOptions: MockCallOptions = {
|
||||
cwd: options.cwd,
|
||||
sessionId: options.sessionId,
|
||||
onStream: options.onStream,
|
||||
setup(config: AgentSetup): ProviderAgent {
|
||||
if (config.claudeAgent) {
|
||||
throw new Error('Claude Code agent calls are not supported by the Mock provider');
|
||||
}
|
||||
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 { 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 {
|
||||
cwd: string;
|
||||
sessionId?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
allowedTools?: string[];
|
||||
/** Maximum number of agentic turns */
|
||||
maxTurns?: number;
|
||||
@ -26,13 +37,14 @@ export interface ProviderCallOptions {
|
||||
openaiApiKey?: string;
|
||||
}
|
||||
|
||||
/** Provider interface - all providers must implement this */
|
||||
export interface Provider {
|
||||
/** Call the provider with a prompt (using systemPrompt from options if provided) */
|
||||
call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
|
||||
/** A configured agent ready to be called */
|
||||
export interface ProviderAgent {
|
||||
call(prompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
|
||||
}
|
||||
|
||||
/** Call the provider with explicit system prompt */
|
||||
callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
|
||||
/** Provider interface — creates configured agents from setup */
|
||||
export interface Provider {
|
||||
setup(config: AgentSetup): ProviderAgent;
|
||||
}
|
||||
|
||||
/** Provider type */
|
||||
|
||||
@ -66,15 +66,15 @@ export class TaskSummarizer {
|
||||
const model = options.model ?? globalConfig.model;
|
||||
|
||||
const provider = getProvider(providerType);
|
||||
const callOptions: SummarizeOptions & { systemPrompt: string; allowedTools: [] } = {
|
||||
cwd: options.cwd,
|
||||
const agent = provider.setup({
|
||||
name: 'summarizer',
|
||||
systemPrompt: loadTemplate('score_slug_system_prompt', 'en'),
|
||||
});
|
||||
const response = await agent.call(taskName, {
|
||||
cwd: options.cwd,
|
||||
model,
|
||||
allowedTools: [],
|
||||
};
|
||||
if (model) {
|
||||
callOptions.model = model;
|
||||
}
|
||||
const response = await provider.call('summarizer', taskName, callOptions);
|
||||
});
|
||||
|
||||
const slug = sanitizeSlug(response.content);
|
||||
log.info('Task name summarized', { original: taskName, slug });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user