feat(tools): support per-tool timeout override for council runs
This commit is contained in:
+17
-1
@@ -3,6 +3,21 @@
|
||||
"updated_at": "2026-02-23",
|
||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||
"plans": {
|
||||
"council-tool-timeout-override": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-23",
|
||||
"updated": "2026-02-23",
|
||||
"summary": "Added optional per-tool timeout override support in ToolExecutor and set `council.run` to 180000ms so council orchestration in the native tool loop is no longer hard-capped by the global 30000ms tool timeout.",
|
||||
"files_modified": [
|
||||
"src/tools/types.ts",
|
||||
"src/tools/executor.ts",
|
||||
"src/tools/executor.test.ts",
|
||||
"src/tools/builtin/council-run.ts",
|
||||
"src/tools/builtin/council-run.test.ts",
|
||||
"docs/plans/state.json"
|
||||
],
|
||||
"test_status": "pnpm test:run src/tools/executor.test.ts src/tools/builtin/council-run.test.ts + pnpm typecheck passing"
|
||||
},
|
||||
"gmail-filter-scope-correction": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-23",
|
||||
@@ -6340,7 +6355,7 @@
|
||||
}
|
||||
},
|
||||
"overall_progress": {
|
||||
"total_test_count": 1965,
|
||||
"total_test_count": 1967,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
@@ -6362,6 +6377,7 @@
|
||||
"gmail_filter_creation": "completed — gmail.filter.create tool added with criteria/action validation; gmail-auth requests explicit gmail.settings.basic + gmail.readonly scopes for filter creation and inbox reads",
|
||||
"toolloop_action_intent_recovery": "completed — when a model claims it will execute a tool but emits no tool call, NativeAgent now issues one internal nudge and continues the same turn to execute tools or produce a concrete blocker",
|
||||
"toolloop_execution_claim_recovery": "completed — when a model claims a known tool already succeeded/failed without emitting a tool call, NativeAgent now nudges once and retries the same turn before returning text",
|
||||
"council_tool_timeout_override": "completed — ToolExecutor supports per-tool timeout overrides and council.run now uses a 180s timeout to avoid false 30s council timeouts in the tool loop",
|
||||
"minimal_tui_multiline_paste_mode": "completed — minimal TUI now supports `/paste`/`/multiline` multiline compose mode ending with single '.' line, preventing newline truncation for pasted prompts",
|
||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback, plus 2026-02-23 arg hydration hardening, tool.args_rewritten audit metric, transient fetch retry/timeout hardening, localhost->127.0.0.1 fallback for transcription endpoint connectivity, and whisper docker-compose entrypoint arg fix for port 18801",
|
||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||
|
||||
@@ -55,6 +55,15 @@ const config = {
|
||||
} as const;
|
||||
|
||||
describe('council.run tool', () => {
|
||||
it('sets extended tool timeout for council orchestration', () => {
|
||||
const tool = createCouncilRunTool({
|
||||
registry: createRegistry(),
|
||||
orchestrator: { delegate: vi.fn() as any },
|
||||
config: config as any,
|
||||
});
|
||||
expect(tool.timeoutMs).toBe(180000);
|
||||
});
|
||||
|
||||
it('runs council pipeline and returns output summary', async () => {
|
||||
const previousDataDir = process.env.FLYNN_DATA_DIR;
|
||||
const testDataDir = mkdtempSync(join(tmpdir(), 'flynn-council-run-'));
|
||||
|
||||
@@ -8,6 +8,8 @@ import { CouncilsOrchestrator, type CouncilsConfig } from '../../councils/orches
|
||||
import type { CouncilScaffold } from '../../councils/scaffold.js';
|
||||
import { councilRunInputSchema } from '../../councils/types.js';
|
||||
|
||||
const COUNCIL_TOOL_TIMEOUT_MS = 180_000;
|
||||
|
||||
interface DelegateRunner {
|
||||
delegate(request: {
|
||||
tier: 'fast' | 'default' | 'complex' | 'local';
|
||||
@@ -152,6 +154,7 @@ export function createCouncilRunTool(deps: CouncilRunDeps): Tool {
|
||||
name: 'council.run',
|
||||
description:
|
||||
'Run the deterministic dual-council pipeline (D/P groups with bridge-only exchange and meta merge).',
|
||||
timeoutMs: COUNCIL_TOOL_TIMEOUT_MS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -21,6 +21,17 @@ const slowTool: Tool = {
|
||||
},
|
||||
};
|
||||
|
||||
const slowToolWithCustomTimeout: Tool = {
|
||||
name: 'test.slow_custom_timeout',
|
||||
description: 'Takes forever with tool-level timeout override',
|
||||
timeoutMs: 120,
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
execute: async () => {
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
return { success: true, output: 'done' };
|
||||
},
|
||||
};
|
||||
|
||||
const failTool: Tool = {
|
||||
name: 'test.fail',
|
||||
description: 'Throws',
|
||||
@@ -160,6 +171,17 @@ describe('ToolExecutor', () => {
|
||||
expect(result.error).toContain('timed out');
|
||||
});
|
||||
|
||||
it('honors per-tool timeout override', async () => {
|
||||
const registry = new ToolRegistry();
|
||||
registry.register(slowToolWithCustomTimeout);
|
||||
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
|
||||
const executor = new ToolExecutor(registry, hooks, { defaultTimeoutMs: 30 });
|
||||
|
||||
const result = await executor.execute('test.slow_custom_timeout', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('timed out after 120ms');
|
||||
});
|
||||
|
||||
it('aborts cancellable tool work on timeout', async () => {
|
||||
const registry = new ToolRegistry();
|
||||
registry.register(cancellableTool);
|
||||
|
||||
@@ -288,6 +288,7 @@ export class ToolExecutor {
|
||||
});
|
||||
|
||||
let timeoutHandle: NodeJS.Timeout | undefined;
|
||||
const timeoutMs = tool.timeoutMs ?? this.defaultTimeoutMs;
|
||||
const timeoutAbortController = new AbortController();
|
||||
const externalSignal = options?.signal;
|
||||
const combinedSignal = externalSignal
|
||||
@@ -332,9 +333,9 @@ export class ToolExecutor {
|
||||
timeoutHandle = setTimeout(
|
||||
() => {
|
||||
timeoutAbortController.abort();
|
||||
reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`));
|
||||
reject(new Error(`Tool '${toolName}' timed out after ${timeoutMs}ms`));
|
||||
},
|
||||
this.defaultTimeoutMs,
|
||||
timeoutMs,
|
||||
);
|
||||
}),
|
||||
...(externalAbortPromise ? [externalAbortPromise] : []),
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: ToolInputSchema;
|
||||
/** Optional per-tool execution timeout in milliseconds. */
|
||||
timeoutMs?: number;
|
||||
/** Secret scopes required to execute this tool (optional). */
|
||||
requiredSecretScopes?: string[];
|
||||
execute(args: unknown, context?: ToolExecutionContext): Promise<ToolResult>;
|
||||
|
||||
Reference in New Issue
Block a user