Provider およびモデル名を出力

This commit is contained in:
nrslib 2026-02-11 10:38:03 +09:00
parent fc1dfcc3c0
commit 69bd77ab62
5 changed files with 114 additions and 5 deletions

View File

@ -27,14 +27,18 @@ const { mockInterruptAllQueries, MockPieceEngine } = vi.hoisted(() => {
class MockPieceEngine extends EE {
private abortRequested = false;
private runResolve: ((value: { status: string; iteration: number }) => void) | null = null;
static lastOptions: { abortSignal?: AbortSignal } | null = null;
constructor(
_config: unknown,
_cwd: string,
_task: string,
_options: unknown,
options: unknown,
) {
super();
if (options && typeof options === 'object') {
MockPieceEngine.lastOptions = options as { abortSignal?: AbortSignal };
}
}
abort(): void {
@ -170,6 +174,7 @@ describe('executePiece: SIGINT handler integration', () => {
beforeEach(() => {
vi.clearAllMocks();
MockPieceEngine.lastOptions = null;
tmpDir = join(tmpdir(), `takt-sigint-it-${randomUUID()}`);
mkdirSync(tmpDir, { recursive: true });
mkdirSync(join(tmpDir, '.takt', 'reports'), { recursive: true });
@ -243,6 +248,30 @@ describe('executePiece: SIGINT handler integration', () => {
expect(result.success).toBe(false);
});
it('should abort provider signal on first SIGINT', async () => {
const config = makeConfig();
const resultPromise = executePiece(config, 'test task', tmpDir, {
projectCwd: tmpDir,
});
await new Promise((resolve) => setTimeout(resolve, 10));
const signal = MockPieceEngine.lastOptions?.abortSignal;
expect(signal).toBeDefined();
expect(signal!.aborted).toBe(false);
const allListeners = process.rawListeners('SIGINT') as ((...args: unknown[]) => void)[];
const newListener = allListeners.find((l) => !savedSigintListeners.includes(l));
expect(newListener).toBeDefined();
newListener!();
expect(signal!.aborted).toBe(true);
const result = await resultPromise;
expect(result.success).toBe(false);
});
it('should register EPIPE handler before calling interruptAllQueries', async () => {
const config = makeConfig();

View File

@ -18,9 +18,11 @@ const { MockPieceEngine, mockLoadPersonaSessions, mockLoadWorktreeSessions } = v
class MockPieceEngine extends EE {
static lastInstance: MockPieceEngine;
readonly receivedOptions: Record<string, unknown>;
private readonly config: PieceConfig;
constructor(config: PieceConfig, _cwd: string, _task: string, options: Record<string, unknown>) {
super();
this.config = config;
this.receivedOptions = options;
MockPieceEngine.lastInstance = this;
}
@ -28,6 +30,10 @@ const { MockPieceEngine, mockLoadPersonaSessions, mockLoadWorktreeSessions } = v
abort(): void {}
async run(): Promise<{ status: string; iteration: number }> {
const firstStep = this.config.movements[0];
if (firstStep) {
this.emit('movement:start', firstStep, 1, firstStep.instructionTemplate);
}
this.emit('piece:complete', { status: 'completed', iteration: 1 });
return { status: 'completed', iteration: 1 };
}
@ -124,6 +130,7 @@ vi.mock('../shared/exitCodes.js', () => ({
}));
import { executePiece } from '../features/tasks/execute/pieceExecution.js';
import { info } from '../shared/ui/index.js';
function makeConfig(): PieceConfig {
return {
@ -218,4 +225,27 @@ describe('executePiece session loading', () => {
// Then: sessions are loaded
expect(mockLoadPersonaSessions).toHaveBeenCalledWith('/tmp/project', 'claude');
});
it('should log provider and model per movement with global defaults', async () => {
await executePiece(makeConfig(), 'task', '/tmp/project', {
projectCwd: '/tmp/project',
});
const mockInfo = vi.mocked(info);
expect(mockInfo).toHaveBeenCalledWith('Provider: claude');
expect(mockInfo).toHaveBeenCalledWith('Model: (default)');
});
it('should log provider and model per movement with overrides', async () => {
await executePiece(makeConfig(), 'task', '/tmp/project', {
projectCwd: '/tmp/project',
provider: 'codex',
model: 'gpt-5',
personaProviders: { coder: 'opencode' },
});
const mockInfo = vi.mocked(info);
expect(mockInfo).toHaveBeenCalledWith('Provider: opencode');
expect(mockInfo).toHaveBeenCalledWith('Model: gpt-5');
});
});

View File

@ -11,6 +11,7 @@ import type { RunAgentOptions } from '../../../agents/runner.js';
import type { PhaseRunnerContext } from '../phase-runner.js';
import type { PieceEngineOptions, PhaseName } from '../types.js';
import { buildSessionKey } from '../session-key.js';
import { resolveMovementProviderModel } from '../provider-resolution.js';
export class OptionsBuilder {
constructor(
@ -30,13 +31,19 @@ export class OptionsBuilder {
const movements = this.getPieceMovements();
const currentIndex = movements.findIndex((m) => m.name === step.name);
const currentPosition = currentIndex >= 0 ? `${currentIndex + 1}/${movements.length}` : '?/?';
const resolved = resolveMovementProviderModel({
step,
provider: this.engineOptions.provider,
model: this.engineOptions.model,
personaProviders: this.engineOptions.personaProviders,
});
return {
cwd: this.getCwd(),
abortSignal: this.engineOptions.abortSignal,
personaPath: step.personaPath,
provider: step.provider ?? this.engineOptions.personaProviders?.[step.personaDisplayName] ?? this.engineOptions.provider,
model: step.model ?? this.engineOptions.model,
provider: resolved.provider,
model: resolved.model,
permissionMode: step.permissionMode,
language: this.getLanguage(),
onStream: this.engineOptions.onStream,

View File

@ -0,0 +1,24 @@
import type { PieceMovement } from '../models/types.js';
export type ProviderType = 'claude' | 'codex' | 'opencode' | 'mock';
export interface MovementProviderModelInput {
step: Pick<PieceMovement, 'provider' | 'model' | 'personaDisplayName'>;
provider?: ProviderType;
model?: string;
personaProviders?: Record<string, ProviderType>;
}
export interface MovementProviderModelOutput {
provider?: ProviderType;
model?: string;
}
export function resolveMovementProviderModel(input: MovementProviderModelInput): MovementProviderModelOutput {
return {
provider: input.step.provider
?? input.personaProviders?.[input.step.personaDisplayName]
?? input.provider,
model: input.step.model ?? input.model,
};
}

View File

@ -63,6 +63,7 @@ import { selectOption, promptInput } from '../../../shared/prompt/index.js';
import { getLabel } from '../../../shared/i18n/index.js';
import { installSigIntHandler } from './sigintHandler.js';
import { buildRunPaths } from '../../../core/piece/run/run-paths.js';
import { resolveMovementProviderModel } from '../../../core/piece/provider-resolution.js';
import { writeFileAtomic, ensureDir } from '../../../infra/config/index.js';
const log = createLogger('piece');
@ -396,10 +397,11 @@ export async function executePiece(
let onAbortSignal: (() => void) | undefined;
let sigintCleanup: (() => void) | undefined;
let onEpipe: ((err: NodeJS.ErrnoException) => void) | undefined;
const runAbortController = new AbortController();
try {
engine = new PieceEngine(pieceConfig, cwd, task, {
abortSignal: options.abortSignal,
abortSignal: runAbortController.signal,
onStream: streamHandler,
onUserInput,
initialSessions: savedSessions,
@ -482,6 +484,16 @@ export async function executePiece(
movementIteration,
});
out.info(`[${iteration}/${pieceConfig.maxMovements}] ${step.name} (${step.personaDisplayName})`);
const resolved = resolveMovementProviderModel({
step,
provider: options.provider,
model: options.model,
personaProviders: options.personaProviders,
});
const movementProvider = resolved.provider ?? currentProvider;
const movementModel = resolved.model ?? globalConfig.model ?? '(default)';
out.info(`Provider: ${movementProvider}`);
out.info(`Model: ${movementModel}`);
// Log prompt content for debugging
if (instruction) {
@ -686,6 +698,9 @@ export async function executePiece(
if (!engine || !onEpipe) {
throw new Error('Abort handler invoked before PieceEngine initialization');
}
if (!runAbortController.signal.aborted) {
runAbortController.abort();
}
process.on('uncaughtException', onEpipe);
interruptAllQueries();
engine.abort();
@ -695,7 +710,11 @@ export async function executePiece(
const useExternalAbort = Boolean(options.abortSignal);
if (useExternalAbort) {
onAbortSignal = abortEngine;
options.abortSignal!.addEventListener('abort', onAbortSignal, { once: true });
if (options.abortSignal!.aborted) {
abortEngine();
} else {
options.abortSignal!.addEventListener('abort', onAbortSignal, { once: true });
}
} else {
const handler = installSigIntHandler(abortEngine);
sigintCleanup = handler.cleanup;