feat: wire claude-code agent runner backend

This commit is contained in:
Andrey Avtomonov 2026-05-15 13:04:16 +02:00
parent eb90d2f32c
commit 3de32c43a1
16 changed files with 229 additions and 21 deletions

View file

@ -12,7 +12,6 @@ import {
agentToolOutputToText,
assertAgentToolSet,
type AgentToolDefinition,
type AgentToolSet,
} from './agent-tool.js';
import type { AgentRunnerPort, RunLoopParams, RunLoopResult, RunLoopStopReason } from './agent-runner.service.js';

View file

@ -5,7 +5,7 @@ import { AgentRunnerService } from '../agent/index.js';
import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../project/index.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FakeSourceAdapter } from './adapters/fake/fake.adapter.js';
import { createLocalBundleIngestRuntime } from './local-bundle-runtime.js';
import { createLocalBundleIngestRuntime, resolveAgentRunnerForTest } from './local-bundle-runtime.js';
type RuntimeWithConnectionDeps = {
deps: {
@ -55,7 +55,7 @@ describe('createLocalBundleIngestRuntime', () => {
}),
).toThrow(
[
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway; llm.agentRunner.backend: claude-code; or an injected agentRunner.',
`Configure an Anthropic provider, then rerun ingest:`,
` ktx setup --project-dir ${project.projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`,
].join('\n'),
@ -84,6 +84,18 @@ describe('createLocalBundleIngestRuntime', () => {
await mkdir(runtime.storage.resolveUploadDir('job-1'), { recursive: true });
});
it('constructs a Claude Agent SDK runner when llm.agentRunner.backend is claude-code', () => {
project.config.llm = {
provider: { backend: 'none' },
models: { default: 'claude-sonnet-4-6' },
agentRunner: { backend: 'claude-code' },
};
const resolved = resolveAgentRunnerForTest({ project, adapters: [] });
expect(resolved.agentRunner.constructor.name).toBe('ClaudeAgentSdkRunnerService');
});
it('exposes canonical warehouse connection types to local ingest SL tools', async () => {
project.config.connections.warehouse = {
driver: 'postgres',

View file

@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
import type { KtxLlmProvider } from '@ktx/llm';
import YAML from 'yaml';
import type { AgentRunnerPort, AgentToolSet } from '../agent/index.js';
import { AgentRunnerService as DefaultAgentRunnerService } from '../agent/index.js';
import { AgentRunnerService as DefaultAgentRunnerService, ClaudeAgentSdkRunnerService } from '../agent/index.js';
import { localConnectionInfoFromConfig, type KtxSqlQueryExecutorPort } from '../connections/index.js';
import type { KtxEmbeddingPort, KtxLogger } from '../core/index.js';
import { noopLogger, SessionWorktreeService } from '../core/index.js';
@ -570,7 +570,7 @@ function nextLocalJobId(): string {
function localIngestLlmProviderGuardMessage(projectDir: string): string {
return [
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway; llm.agentRunner.backend: claude-code; or an injected agentRunner.',
'Configure an Anthropic provider, then rerun ingest:',
` ktx setup --project-dir ${projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`,
].join('\n');
@ -587,6 +587,17 @@ function resolveAgentRunner(options: CreateLocalBundleIngestRuntimeOptions): {
return { agentRunner: options.agentRunner, ...(llmProvider ? { llmProvider } : {}) };
}
if (options.project.config.llm.agentRunner.backend === 'claude-code') {
return {
agentRunner: new ClaudeAgentSdkRunnerService({
projectDir: options.project.projectDir,
modelSlots: options.project.config.llm.models,
logger: options.logger ?? noopLogger,
}),
...(llmProvider ? { llmProvider } : {}),
};
}
if (!llmProvider) {
throw new Error(localIngestLlmProviderGuardMessage(options.project.projectDir));
}
@ -603,6 +614,8 @@ function resolveAgentRunner(options: CreateLocalBundleIngestRuntimeOptions): {
};
}
export const resolveAgentRunnerForTest = resolveAgentRunner;
export function createLocalBundleIngestRuntime(
options: CreateLocalBundleIngestRuntimeOptions,
): LocalBundleIngestRuntime {

View file

@ -143,6 +143,17 @@ describe('createLocalProjectMemoryCapture', () => {
);
});
it('constructs local memory capture with Claude Agent SDK runner config', async () => {
const project = await initKtxProject({ projectDir: tempDir });
project.config.llm = {
provider: { backend: 'none' },
models: { default: 'claude-sonnet-4-6' },
agentRunner: { backend: 'claude-code' },
};
expect(() => createLocalProjectMemoryCapture(project)).not.toThrow();
});
it('captures a semantic-layer source for a named local connection id', async () => {
const project = await initKtxProject({ projectDir: tempDir });
project.config.connections.warehouse = { driver: 'postgres' };

View file

@ -2,7 +2,7 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { KtxLlmProvider } from '@ktx/llm';
import YAML from 'yaml';
import { AgentRunnerService, type AgentRunnerPort } from '../agent/index.js';
import { AgentRunnerService, ClaudeAgentSdkRunnerService, type AgentRunnerPort } from '../agent/index.js';
import { localConnectionInfoFromConfig } from '../connections/index.js';
import type { KtxEmbeddingPort, KtxFileStorePort, KtxFileWriteResult } from '../core/index.js';
import { type KtxLogger, noopLogger, SessionWorktreeService } from '../core/index.js';
@ -104,10 +104,16 @@ export function createLocalProjectMemoryCapture(
});
const agentRunner =
options.agentRunner ??
new AgentRunnerService({
llmProvider: requireLlmProvider(llmProvider),
logger,
});
(project.config.llm.agentRunner.backend === 'claude-code'
? new ClaudeAgentSdkRunnerService({
projectDir: project.projectDir,
modelSlots: project.config.llm.models,
logger,
})
: new AgentRunnerService({
llmProvider: requireLlmProvider(llmProvider),
logger,
}));
const memoryAgent = new MemoryAgentService({
settings: {
knowledge: { userScopedKnowledgeEnabled: false },
@ -145,7 +151,9 @@ export function createLocalProjectMemoryCapture(
function requireLlmProvider(provider: KtxLlmProvider | null | undefined): KtxLlmProvider {
if (!provider) {
throw new Error('createLocalProjectMemoryCapture requires llm.provider.backend or an injected agentRunner');
throw new Error(
'createLocalProjectMemoryCapture requires llm.provider.backend, llm.agentRunner.backend: claude-code, or an injected agentRunner',
);
}
return provider;
}