fix: allow agent setup without context

This commit is contained in:
Andrey Avtomonov 2026-05-19 11:58:06 +02:00
parent eb41d084af
commit fa01c5fb90
9 changed files with 54 additions and 78 deletions

View file

@ -26,7 +26,7 @@ below.
| Flag | Description | Default |
|------|-------------|---------|
| `--agents` | Install agent integration only | `false` |
| `--agents` | Install agent configuration and rules only | `false` |
| `--target <target>` | Agent target: `claude-code`, `codex`, `cursor`, `opencode`, or `universal` | - |
| `--global` | Install agent integration into the global target scope for `claude-code` or `codex` | `false` |
| `--yes` | Accept safe defaults in non-interactive setup | `false` |
@ -82,15 +82,19 @@ embedding credential source.
### Runtime
Setup prepares the managed Python runtime when your selected configuration
needs it. The runtime step runs after database and source setup and before the
initial context build.
needs it. In the full setup flow, the runtime step runs after database and
source setup and before the initial context build.
KTX prepares the `core` runtime feature when agent integration, query-history
ingest, Looker source ingest, or daemon-backed context build paths need it. KTX
prepares the `local-embeddings` runtime feature when you choose managed local
`sentence-transformers` embeddings. Existing external daemon URLs, such as
`KTX_DAEMON_URL` or `KTX_SQL_ANALYSIS_URL`, satisfy the matching dependency and
skip managed runtime installation for that dependency.
KTX prepares the `core` runtime feature when query-history ingest, Looker
source ingest, database introspection fallback, or daemon-backed context build
paths need it. KTX prepares the `local-embeddings` runtime feature when you
choose managed local `sentence-transformers` embeddings. Existing external
daemon URLs, such as `KTX_DAEMON_URL` or `KTX_SQL_ANALYSIS_URL`, satisfy the
matching dependency and skip managed runtime installation for that dependency.
`ktx setup --agents` doesn't prepare runtime features or build context. It only
installs agent configuration and rules. Start MCP with `ktx mcp start` before
using HTTP-based agents; MCP startup prepares the runtime it needs.
Interactive setup prompts before installing runtime features. Use `--yes` to
install them without prompting. Use `--no-input` to fail fast when required

View file

@ -13,18 +13,18 @@ a developer or operator agent also needs pinned `ktx` admin commands.
## Install with setup
Start the MCP server before connecting an end-user agent:
```bash
ktx mcp start
```
Then install client integration:
Install client integration first:
```bash
ktx setup --agents
```
Then start the MCP server before using HTTP-based clients:
```bash
ktx mcp start
```
Use `--target` for one target:
```bash

View file

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -7,10 +7,10 @@ import {
} from './runtime-requirements.js';
describe('runtime requirement detection', () => {
it('requires core for agent/MCP setup', () => {
it('does not require runtime for agent/MCP setup alone', () => {
const config = buildDefaultKtxProjectConfig();
expect(resolveProjectRuntimeRequirements(config, { agents: true }).features).toEqual(['core']);
expect(resolveProjectRuntimeRequirements(config).features).toEqual([]);
});
it('requires core for Looker source ingest unless an external daemon is configured', () => {

View file

@ -8,7 +8,6 @@ import type { KtxRuntimeFeature } from './managed-python-runtime.js';
import type { KtxPublicIngestPlan } from './public-ingest.js';
type KtxRuntimeRequirementReason =
| 'agent-mcp'
| 'query-history'
| 'looker-source'
| 'database-introspection'
@ -26,7 +25,6 @@ export interface KtxRuntimeRequirements {
}
export interface KtxProjectRuntimeRequirementOptions {
agents?: boolean;
databaseIntrospectionFallback?: boolean;
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
}
@ -92,14 +90,6 @@ export function resolveProjectRuntimeRequirements(
const env = options.env ?? process.env;
const requirements: KtxRuntimeRequirement[] = [];
if (options.agents === true) {
requirements.push({
feature: 'core',
reason: 'agent-mcp',
detail: 'Agent MCP setup uses semantic-layer query tools and SQL validation.',
});
}
if (options.databaseIntrospectionFallback === true && !hasDaemonOverride(env)) {
requirements.push({
feature: 'core',

View file

@ -4,7 +4,6 @@ import { join } from 'node:path';
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
import { buildDefaultKtxProjectConfig, readKtxSetupState, type KtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ManagedPythonCommandRuntime } from './managed-python-command.js';
import { runKtxSetupRuntimeStep } from './setup-runtime.js';
function makeIo() {
@ -43,9 +42,9 @@ describe('runKtxSetupRuntimeStep', () => {
await rm(tempDir, { recursive: true, force: true });
});
it('ensures core runtime for agent setup and records the runtime step', async () => {
it('skips runtime setup when the project has no direct runtime requirements', async () => {
const io = makeIo();
const ensureRuntime = vi.fn(async (): Promise<ManagedPythonCommandRuntime> => ({} as ManagedPythonCommandRuntime));
const ensureRuntime = vi.fn();
await expect(
runKtxSetupRuntimeStep(
@ -54,7 +53,6 @@ describe('runKtxSetupRuntimeStep', () => {
inputMode: 'auto',
cliVersion: '0.2.0',
runtimeInstallPolicy: 'prompt',
agents: true,
},
io.io,
{
@ -63,17 +61,11 @@ describe('runKtxSetupRuntimeStep', () => {
env: {},
},
),
).resolves.toMatchObject({ status: 'ready' });
).resolves.toMatchObject({ status: 'skipped' });
expect(ensureRuntime).toHaveBeenCalledWith(
expect.objectContaining({
cliVersion: '0.2.0',
installPolicy: 'prompt',
feature: 'core',
}),
);
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('runtime');
expect(io.stdout()).toContain('Runtime ready: yes (core)');
expect(ensureRuntime).not.toHaveBeenCalled();
expect((await readKtxSetupState(tempDir)).completed_steps).not.toContain('runtime');
expect(io.stdout()).toContain('Runtime setup skipped.');
});
it('fails fast when required runtime features cannot be installed in no-input mode', async () => {
@ -89,7 +81,7 @@ describe('runKtxSetupRuntimeStep', () => {
inputMode: 'disabled',
cliVersion: '0.2.0',
runtimeInstallPolicy: 'never',
agents: true,
databaseIntrospectionFallback: true,
},
io.io,
{
@ -131,7 +123,6 @@ describe('runKtxSetupRuntimeStep', () => {
inputMode: 'auto',
cliVersion: '0.2.0',
runtimeInstallPolicy: 'auto',
agents: false,
},
io.io,
{

View file

@ -24,7 +24,6 @@ export interface KtxSetupRuntimeArgs {
inputMode: 'auto' | 'disabled';
cliVersion: string;
runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
agents: boolean;
databaseIntrospectionFallback?: boolean;
}
@ -62,7 +61,6 @@ export async function runKtxSetupRuntimeStep(
const loadProjectForRuntime = deps.loadProject ?? loadKtxProject;
const project = await loadProjectForRuntime({ projectDir: args.projectDir });
const requirements = resolveProjectRuntimeRequirements(project.config, {
agents: args.agents,
databaseIntrospectionFallback: args.databaseIntrospectionFallback,
env: deps.env ?? process.env,
});

View file

@ -1733,7 +1733,7 @@ describe('setup status', () => {
expect(committedConfig.stdout).toContain('warehouse:');
});
it('runs agent setup after context succeeds in --agents mode', async () => {
it('runs agent setup without runtime or context in --agents mode', async () => {
const calls: string[] = [];
const io = makeIo();
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
@ -1765,11 +1765,11 @@ describe('setup status', () => {
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
runtime: async () => {
calls.push('runtime');
return runtimeReady(tempDir);
throw new Error('runtime should not run');
},
context: async () => {
calls.push('context');
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
throw new Error('context should not run');
},
agents: async () => {
calls.push('agents');
@ -1783,11 +1783,13 @@ describe('setup status', () => {
),
).resolves.toBe(0);
expect(calls).toEqual(['runtime', 'context', 'agents']);
expect(calls).toEqual(['agents']);
});
it('does not install agents when non-interactive --agents finds context incomplete', async () => {
it('installs agents when non-interactive --agents finds context incomplete', async () => {
const io = makeIo();
const runtime = vi.fn(async () => runtimeReady(tempDir));
const context = vi.fn(async () => ({ status: 'skipped' as const, projectDir: tempDir }));
const agents = vi.fn(async () => ({
status: 'ready' as const,
projectDir: tempDir,
@ -1816,15 +1818,17 @@ describe('setup status', () => {
},
io.io,
{
runtime: async () => runtimeReady(tempDir),
context: async () => ({ status: 'skipped', projectDir: tempDir }),
runtime,
context,
agents,
},
),
).resolves.toBe(1);
).resolves.toBe(0);
expect(agents).not.toHaveBeenCalled();
expect(io.stderr()).toContain('KTX context is not ready for agents.');
expect(runtime).not.toHaveBeenCalled();
expect(context).not.toHaveBeenCalled();
expect(agents).toHaveBeenCalledTimes(1);
expect(io.stderr()).not.toContain('KTX context is not ready for agents.');
});
it('routes a ready project menu selection to agent setup', async () => {
@ -1945,7 +1949,7 @@ describe('setup status', () => {
}
}
expect(calls).toEqual(['runtime', 'agents']);
expect(calls).toEqual(['agents']);
});
it('skips to agent setup when context is ready but agents are not configured', async () => {
@ -2042,10 +2046,10 @@ describe('setup status', () => {
).resolves.toBe(0);
expect(readyMenuSelect).not.toHaveBeenCalled();
expect(calls).toEqual(['runtime', 'agents']);
expect(calls).toEqual(['agents']);
});
it('runs only project resolution, runtime, context gate, and agent setup in --agents mode', async () => {
it('runs only project resolution and agent setup in --agents mode', async () => {
const io = makeIo();
const runtime = vi.fn(async () => runtimeReady(tempDir));
const context = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir, runId: 'setup-context-local-test' }));
@ -2086,8 +2090,8 @@ describe('setup status', () => {
),
).resolves.toBe(0);
expect(runtime).toHaveBeenCalledTimes(1);
expect(context).toHaveBeenCalledTimes(1);
expect(runtime).not.toHaveBeenCalled();
expect(context).not.toHaveBeenCalled();
expect(agents).toHaveBeenCalledTimes(1);
});

View file

@ -339,7 +339,6 @@ export async function readKtxSetupStatus(
}
const agents = [...agentMap.values()];
const runtimeRequirements = resolveProjectRuntimeRequirements(project.config, {
agents: agents.length > 0,
env: options.env ?? process.env,
});
let runtimeReady = runtimeRequirements.features.length === 0 || completedSteps.includes('runtime');
@ -493,12 +492,6 @@ function shouldPrintConciseReadySummary(status: KtxSetupStatus): boolean {
return setupStatusReady(status) && setupContextReady(status) && status.agents.some((agent) => agent.ready);
}
function writeContextNotReadyForAgents(projectDir: string, io: KtxCliIo): void {
io.stderr.write('KTX context is not ready for agents.\n\n');
io.stderr.write(`Build context first:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
io.stderr.write(`Then install agent integration:\n ktx setup --agents --project-dir ${resolve(projectDir)}\n`);
}
function setupRuntimeInstallPolicy(args: Extract<KtxSetupArgs, { command: 'run' }>): 'prompt' | 'auto' | 'never' {
if (args.yes) {
return 'auto';
@ -587,18 +580,19 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
}
const runOnly = readyAction;
const agentOnlySetup = agentsRequested || runOnly === 'agents';
const shouldRunModels = !runOnly || runOnly === 'models';
const shouldRunEmbeddings = !runOnly || runOnly === 'embeddings';
const shouldRunDatabases = !runOnly || runOnly === 'databases';
const shouldRunSources = !runOnly || runOnly === 'sources';
const shouldRunRuntime =
agentsRequested || !runOnly || runOnly === 'runtime' || runOnly === 'context' || runOnly === 'agents';
const shouldRunContext = agentsRequested || !runOnly || runOnly === 'context';
!agentOnlySetup && (!runOnly || runOnly === 'runtime' || runOnly === 'context');
const shouldRunContext = !agentOnlySetup && (!runOnly || runOnly === 'context');
const shouldRunAgents = agentsRequested || !runOnly || runOnly === 'agents';
const showPromptInstructions = projectResult.confirmedCreation !== true;
const setupSteps: KtxSetupFlowStep[] = agentsRequested
? ['runtime', 'context']
const setupSteps: KtxSetupFlowStep[] = agentOnlySetup
? []
: ['models', 'embeddings', 'databases', 'sources', 'runtime', 'context'];
if (shouldRunAgents && args.skipAgents !== true) {
setupSteps.push('agents');
@ -737,7 +731,6 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
inputMode: args.inputMode,
cliVersion: args.cliVersion,
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
agents: shouldRunAgents && args.skipAgents !== true,
},
io,
);
@ -799,10 +792,6 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
}
if (step === 'context' && stepResult.status !== 'ready') {
if (shouldRunAgents && args.skipAgents !== true) {
if (agentsRequested) {
writeContextNotReadyForAgents(projectResult.projectDir, io);
return args.inputMode === 'disabled' ? 1 : 0;
}
return 0;
}
}