mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-25 08:48:08 +02:00
feat: add claude-code llm backend with runtime port (#115)
* docs: revise claude-code ingest backend spec * docs: keep claude-code spec focused on ingest * docs: expand claude-code spec to full llm parity * Refine claude-code backend spec after adversarial review iteration 1 * Refine claude-code backend spec after adversarial review iteration 2 * Refine claude-code backend spec after adversarial review iteration 3 * feat: recognize claude-code llm backend * feat: add ktx llm runtime port * feat: add claude-code llm runtime * feat: route non-agent llm calls through runtime * feat: run ingest agents through llm runtime * feat: support claude-code setup and status * test: verify claude-code backend runtime * docs: add claude-code backend v1 runtime plan * fix: close claude-code runtime isolation checks * fix: warn on claude-code prompt caching during setup * chore: verify claude-code v1 closure * docs: add claude-code backend v1 isolation closure plan * fix: update claude-code ingest setup guidance * docs: add claude-code backend v1 ingest guidance closure plan * docs: align claude-code isolation spec with sdk metadata * test: cover claude-code host discovery metadata * fix: tolerate claude-code host discovery metadata * docs: clarify claude-code host discovery metadata * docs: add claude-code auth-probe isolation fix plan * chore: prepare kaelio ktx rc1 release * chore: add semantic release workflow * fix: unblock ci checks * chore(release): 0.1.0-rc.1 * feat: add Claude Code model selection to setup * fix: keep git maintenance attached in local repos
This commit is contained in:
parent
e6d578c03f
commit
b565e44a22
109 changed files with 10218 additions and 1093 deletions
|
|
@ -1,13 +1,17 @@
|
|||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import YAML from 'yaml';
|
||||
import { AgentRunnerService } 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';
|
||||
import type { KtxSemanticLayerComputePort } from '../daemon/index.js';
|
||||
import { createLocalKtxLlmProviderFromConfig } from '../llm/index.js';
|
||||
import {
|
||||
createLocalKtxLlmRuntimeFromConfig,
|
||||
RuntimeAgentRunner,
|
||||
type AgentRunnerPort,
|
||||
type KtxLlmRuntimePort,
|
||||
type KtxRuntimeToolSet,
|
||||
} from '../llm/index.js';
|
||||
import type { KtxLocalProject } from '../project/index.js';
|
||||
import { PromptService } from '../prompts/index.js';
|
||||
import { SkillsRegistryService } from '../skills/index.js';
|
||||
|
|
@ -63,8 +67,8 @@ const LOCAL_AUTHOR = { name: 'KTX Local', email: 'local@ktx.local' };
|
|||
const LOCAL_SHAPE_WARNING = 'Local memory ingest validates semantic-layer YAML shape only.';
|
||||
|
||||
export interface CreateLocalProjectMemoryIngestOptions {
|
||||
llmProvider?: KtxLlmProvider;
|
||||
agentRunner?: AgentRunnerService;
|
||||
llmRuntime?: KtxLlmRuntimePort;
|
||||
agentRunner?: AgentRunnerPort;
|
||||
memoryModel?: string;
|
||||
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
||||
queryExecutor?: { execute(input: { connectionId: string; sql: string; maxRows?: number }): Promise<KtxQueryResult> };
|
||||
|
|
@ -89,7 +93,8 @@ export function createLocalProjectMemoryIngest(
|
|||
const slSearchService = new SlSearchService(embedding, slSourcesRepository, logger);
|
||||
const wikiService = new KnowledgeWikiService(rootFileStore, embedding, knowledgeIndex, project.git, logger);
|
||||
const authorResolver = new LocalAuthorResolver();
|
||||
const llmProvider = options.llmProvider ?? createLocalKtxLlmProviderFromConfig(project.config.llm);
|
||||
const llmRuntime =
|
||||
options.llmRuntime ?? createLocalKtxLlmRuntimeFromConfig(project.config.llm, { projectDir: project.projectDir });
|
||||
const toolsetFactory = new LocalMemoryToolsetFactory({
|
||||
project,
|
||||
embedding,
|
||||
|
|
@ -104,10 +109,7 @@ export function createLocalProjectMemoryIngest(
|
|||
});
|
||||
const agentRunner =
|
||||
options.agentRunner ??
|
||||
new AgentRunnerService({
|
||||
llmProvider: requireLlmProvider(llmProvider),
|
||||
logger,
|
||||
});
|
||||
new RuntimeAgentRunner(requireLlmRuntime(llmRuntime));
|
||||
const memoryAgent = new MemoryAgentService({
|
||||
settings: {
|
||||
knowledge: { userScopedKnowledgeEnabled: false },
|
||||
|
|
@ -143,11 +145,11 @@ export function createLocalProjectMemoryIngest(
|
|||
});
|
||||
}
|
||||
|
||||
function requireLlmProvider(provider: KtxLlmProvider | null | undefined): KtxLlmProvider {
|
||||
if (!provider) {
|
||||
function requireLlmRuntime(runtime: KtxLlmRuntimePort | null | undefined): KtxLlmRuntimePort {
|
||||
if (!runtime) {
|
||||
throw new Error('createLocalProjectMemoryIngest requires llm.provider.backend or an injected agentRunner');
|
||||
}
|
||||
return provider;
|
||||
return runtime;
|
||||
}
|
||||
|
||||
class LocalMemoryFileStore implements MemoryFileStorePort {
|
||||
|
|
@ -386,8 +388,8 @@ class LocalShapeOnlySlValidator implements SlValidatorPort<SlValidationDeps> {
|
|||
class LocalMemoryToolSet implements MemoryToolSetLike {
|
||||
constructor(private readonly tools: BaseTool[]) {}
|
||||
|
||||
toAiSdkTools(context: ToolContext) {
|
||||
return Object.fromEntries(this.tools.map((tool) => [tool.name, tool.toAiSdkTool(context)]));
|
||||
toRuntimeTools(context: ToolContext): KtxRuntimeToolSet {
|
||||
return Object.fromEntries(this.tools.map((tool) => [tool.name, tool.toRuntimeTool(context)]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Module-level mock for 'ai' so generateText is a stub. This file is separate from
|
||||
|
|
@ -15,7 +18,6 @@ import { MemoryAgentService } from './memory-agent.service.js';
|
|||
|
||||
interface BuiltMocks {
|
||||
appSettings: any;
|
||||
llmProvider: any;
|
||||
prompt: any;
|
||||
eventTracker: any;
|
||||
telemetry: any;
|
||||
|
|
@ -63,7 +65,6 @@ const buildMocks = (overrides: Partial<BuiltMocks> = {}): BuiltMocks => {
|
|||
llm: { memoryIngestionModel: 'test-model' },
|
||||
},
|
||||
},
|
||||
llmProvider: { getModel: vi.fn().mockReturnValue({}) },
|
||||
prompt: { loadPrompt: vi.fn().mockResolvedValue('base framing') },
|
||||
eventTracker: { trackEvent: vi.fn(), createTelemetryIntegration: vi.fn().mockReturnValue(undefined) },
|
||||
telemetry: {
|
||||
|
|
@ -124,11 +125,11 @@ const buildMocks = (overrides: Partial<BuiltMocks> = {}): BuiltMocks => {
|
|||
slValidator: { validateSingleSource: vi.fn().mockResolvedValue({ errors: [], warnings: [] }) },
|
||||
toolsetFactory: {
|
||||
createIngestWuToolset: vi.fn().mockReturnValue({
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toRuntimeTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
createToolset: vi.fn().mockReturnValue({
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toRuntimeTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
},
|
||||
|
|
@ -241,6 +242,39 @@ describe('MemoryAgentService.ingest — session-branch orchestration', () => {
|
|||
expect(result.commitHash).toBe('cafebabe');
|
||||
});
|
||||
|
||||
it('normalizes load_skill output to markdown while preserving structured payload', async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-memory-skill-'));
|
||||
const skillDir = join(tempDir, 'memory_agent');
|
||||
await mkdir(skillDir, { recursive: true });
|
||||
await writeFile(join(skillDir, 'SKILL.md'), '---\nname: memory_agent\n---\nSkill body', 'utf-8');
|
||||
try {
|
||||
const agentRunner = {
|
||||
runLoop: vi.fn(async (params: any) => {
|
||||
const result = await params.toolSet.load_skill.execute({ name: 'memory_agent' });
|
||||
expect(result.markdown).toContain('memory_agent');
|
||||
expect(result.structured).toMatchObject({ name: 'memory_agent' });
|
||||
return { stopReason: 'natural' as const };
|
||||
}),
|
||||
};
|
||||
const mocks = buildMocks({
|
||||
agentRunner,
|
||||
skillsRegistry: {
|
||||
listSkills: vi.fn().mockResolvedValue([{ name: 'memory_agent', path: skillDir }]),
|
||||
buildSkillsPrompt: vi.fn().mockReturnValue(''),
|
||||
getSkill: vi.fn().mockResolvedValue({ name: 'memory_agent', path: skillDir }),
|
||||
stripFrontmatter: vi.fn().mockReturnValue('Skill body'),
|
||||
},
|
||||
});
|
||||
const svc = buildService(mocks);
|
||||
|
||||
await svc.ingest(baseInput);
|
||||
|
||||
expect(agentRunner.runLoop).toHaveBeenCalled();
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('logs prompt debug output when KTX_MEMORY_AGENT_DEBUG_PROMPTS is enabled', async () => {
|
||||
const previousDebugPrompts = process.env.KTX_MEMORY_AGENT_DEBUG_PROMPTS;
|
||||
const mocks = buildMocks();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tool } from 'ai';
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
import { type KtxLogger, noopLogger } from '../core/index.js';
|
||||
import type { KtxRuntimeToolSet } from '../llm/index.js';
|
||||
import {
|
||||
revertSourceToPreHead,
|
||||
type SemanticLayerSource,
|
||||
|
|
@ -125,8 +125,9 @@ export class MemoryAgentService {
|
|||
session: toolSession,
|
||||
};
|
||||
|
||||
const loadSkillTool = {
|
||||
load_skill: tool({
|
||||
const loadSkillTool: KtxRuntimeToolSet = {
|
||||
load_skill: {
|
||||
name: 'load_skill',
|
||||
description:
|
||||
'Load a skill to get specialized instructions. Call this when a skill listed in the system prompt matches the current task.',
|
||||
inputSchema: z.object({
|
||||
|
|
@ -137,23 +138,27 @@ export class MemoryAgentService {
|
|||
if (!skill) {
|
||||
const available =
|
||||
(await this.deps.skillsRegistry.listSkills('memory_agent')).map((s) => s.name).join(', ') || '(none)';
|
||||
return `Skill "${name}" not available to the memory agent. Available: ${available}`;
|
||||
return { markdown: `Skill "${name}" not available to the memory agent. Available: ${available}` };
|
||||
}
|
||||
try {
|
||||
const body = await readFile(join(skill.path, 'SKILL.md'), 'utf-8');
|
||||
if (!skillsLoaded.includes(skill.name)) {
|
||||
skillsLoaded.push(skill.name);
|
||||
}
|
||||
return {
|
||||
const structured = {
|
||||
name: skill.name,
|
||||
skillDirectory: skill.path,
|
||||
content: this.deps.skillsRegistry.stripFrontmatter(body),
|
||||
};
|
||||
return {
|
||||
markdown: `# ${structured.name}\n\n${structured.content}`,
|
||||
structured,
|
||||
};
|
||||
} catch (e) {
|
||||
return `Error loading skill "${name}": ${e instanceof Error ? e.message : String(e)}`;
|
||||
return { markdown: `Error loading skill "${name}": ${e instanceof Error ? e.message : String(e)}` };
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const skillNames: string[] = [...DEFAULT_SKILL_NAMES];
|
||||
|
|
@ -212,7 +217,7 @@ export class MemoryAgentService {
|
|||
modelRole: 'candidateExtraction',
|
||||
systemPrompt,
|
||||
userPrompt: prompt,
|
||||
toolSet: { ...toolset.toAiSdkTools(toolContext), ...loadSkillTool },
|
||||
toolSet: { ...toolset.toRuntimeTools(toolContext), ...loadSkillTool },
|
||||
stepBudget,
|
||||
telemetryTags: {
|
||||
operationName: 'memory-agent-ingest',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { Tool } from 'ai';
|
||||
import type { AgentRunnerService } from '../agent/index.js';
|
||||
import type { AgentRunnerPort, KtxRuntimeToolSet } from '../llm/index.js';
|
||||
import type { GitService, KtxFileStorePort, KtxLogger, SessionWorktreeService } from '../core/index.js';
|
||||
import type { PromptService } from '../prompts/index.js';
|
||||
import type { SkillsRegistryService } from '../skills/index.js';
|
||||
|
|
@ -118,7 +117,7 @@ export interface MemoryCommitMessagePort {
|
|||
export interface MemoryFileStorePort extends KtxFileStorePort<MemoryFileStorePort>, MemoryCommitMessagePort {}
|
||||
|
||||
export interface MemoryToolSetLike {
|
||||
toAiSdkTools(context: ToolContext): Record<string, Tool>;
|
||||
toRuntimeTools(context: ToolContext): KtxRuntimeToolSet;
|
||||
}
|
||||
|
||||
export interface MemoryToolsetFactoryPort {
|
||||
|
|
@ -150,7 +149,7 @@ export interface MemoryAgentServiceDeps {
|
|||
slSourcesRepository: SlSourcesIndexPort;
|
||||
sessionWorktreeService: SessionWorktreeService<MemoryFileStorePort>;
|
||||
semanticLayerSourceReconciler: MemorySlSourceReconcilerPort;
|
||||
agentRunner: AgentRunnerService;
|
||||
agentRunner: AgentRunnerPort;
|
||||
slValidator: SlValidatorPort<SlValidationDeps>;
|
||||
toolsetFactory: MemoryToolsetFactoryPort;
|
||||
telemetry?: MemoryTelemetryPort;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue