mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
refactor(cli): rename text ingest memory port
This commit is contained in:
parent
6392926bdc
commit
57bdc0a19a
2 changed files with 55 additions and 55 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { MemoryCaptureStatus } from '@ktx/context/memory';
|
||||
import type { MemoryIngestStatus } from '@ktx/context/memory';
|
||||
import type { KtxLocalProject } from '@ktx/context/project';
|
||||
import { runKtxTextIngest, type TextMemoryCapturePort } from './text-ingest.js';
|
||||
import { runKtxTextIngest, type TextMemoryIngestPort } from './text-ingest.js';
|
||||
|
||||
function makeIo(options: { isTTY?: boolean } = {}) {
|
||||
let stdout = '';
|
||||
|
|
@ -25,18 +25,18 @@ function makeIo(options: { isTTY?: boolean } = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
function fakeCapture(
|
||||
function fakeIngest(
|
||||
options: {
|
||||
failRunIds?: Set<string>;
|
||||
missingStatusRunIds?: Set<string>;
|
||||
events?: string[];
|
||||
} = {},
|
||||
): TextMemoryCapturePort {
|
||||
): TextMemoryIngestPort {
|
||||
let next = 1;
|
||||
return {
|
||||
capture: vi.fn(async () => {
|
||||
ingest: vi.fn(async () => {
|
||||
const runId = `run-${next++}`;
|
||||
options.events?.push(`capture:${runId}`);
|
||||
options.events?.push(`ingest:${runId}`);
|
||||
return { runId };
|
||||
}),
|
||||
waitForRun: vi.fn(async (runId: string) => {
|
||||
|
|
@ -51,26 +51,26 @@ function fakeCapture(
|
|||
return {
|
||||
runId,
|
||||
status: 'error',
|
||||
stage: 'capturing',
|
||||
stage: 'ingesting',
|
||||
done: true,
|
||||
captured: { wiki: [], sl: [], xrefs: [] },
|
||||
error: `${runId} failed`,
|
||||
commitHash: null,
|
||||
skillsLoaded: [],
|
||||
signalDetected: false,
|
||||
} satisfies MemoryCaptureStatus;
|
||||
} satisfies MemoryIngestStatus;
|
||||
}
|
||||
return {
|
||||
runId,
|
||||
status: 'done',
|
||||
stage: 'capturing',
|
||||
stage: 'ingesting',
|
||||
done: true,
|
||||
captured: { wiki: [`wiki-${runId}`], sl: [`sl-${runId}`], xrefs: [] },
|
||||
error: null,
|
||||
commitHash: `commit-${runId}`,
|
||||
skillsLoaded: ['wiki_capture', 'sl'],
|
||||
signalDetected: true,
|
||||
} satisfies MemoryCaptureStatus;
|
||||
} satisfies MemoryIngestStatus;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -80,11 +80,11 @@ function fakeProject(projectDir = '/tmp/project'): KtxLocalProject {
|
|||
}
|
||||
|
||||
describe('runKtxTextIngest', () => {
|
||||
it('captures repeated inline text sequentially with generated internal chat ids', async () => {
|
||||
it('ingests repeated inline text sequentially with generated internal chat ids', async () => {
|
||||
const io = makeIo();
|
||||
const events: string[] = [];
|
||||
const capture = fakeCapture({ events });
|
||||
const createMemoryCapture = vi.fn(() => capture);
|
||||
const ingest = fakeIngest({ events });
|
||||
const createMemoryIngest = vi.fn(() => ingest);
|
||||
|
||||
await expect(
|
||||
runKtxTextIngest(
|
||||
|
|
@ -99,14 +99,14 @@ describe('runKtxTextIngest', () => {
|
|||
io.io,
|
||||
{
|
||||
loadProject: vi.fn(async () => fakeProject()),
|
||||
createMemoryCapture,
|
||||
createMemoryIngest,
|
||||
now: () => 1_700_000_000_000,
|
||||
},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(createMemoryCapture).toHaveBeenCalledWith({ projectDir: '/tmp/project' });
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(createMemoryIngest).toHaveBeenCalledWith({ projectDir: '/tmp/project' });
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
userId: 'local-cli',
|
||||
|
|
@ -116,7 +116,7 @@ describe('runKtxTextIngest', () => {
|
|||
sourceType: 'external_ingest',
|
||||
}),
|
||||
);
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
chatId: 'cli-text-ingest-1700000000000-2',
|
||||
|
|
@ -124,8 +124,8 @@ describe('runKtxTextIngest', () => {
|
|||
assistantMessage: 'Orders are completed purchases.',
|
||||
}),
|
||||
);
|
||||
expect(capture.capture).not.toHaveBeenCalledWith(expect.objectContaining({ connectionId: expect.anything() }));
|
||||
expect(events).toEqual(['capture:run-1', 'wait:run-1', 'status:run-1', 'capture:run-2', 'wait:run-2', 'status:run-2']);
|
||||
expect(ingest.ingest).not.toHaveBeenCalledWith(expect.objectContaining({ connectionId: expect.anything() }));
|
||||
expect(events).toEqual(['ingest:run-1', 'wait:run-1', 'status:run-1', 'ingest:run-2', 'wait:run-2', 'status:run-2']);
|
||||
expect(JSON.parse(io.stdout())).toMatchObject({
|
||||
status: 'done',
|
||||
results: [
|
||||
|
|
@ -147,7 +147,7 @@ describe('runKtxTextIngest', () => {
|
|||
|
||||
it('loads files and stdin as batch items and passes a global connection id', async () => {
|
||||
const io = makeIo();
|
||||
const capture = fakeCapture();
|
||||
const ingest = fakeIngest();
|
||||
|
||||
await expect(
|
||||
runKtxTextIngest(
|
||||
|
|
@ -163,7 +163,7 @@ describe('runKtxTextIngest', () => {
|
|||
io.io,
|
||||
{
|
||||
loadProject: vi.fn(async () => fakeProject()),
|
||||
createMemoryCapture: vi.fn(() => capture),
|
||||
createMemoryIngest: vi.fn(() => ingest),
|
||||
readFile: vi.fn(async (path) => `file:${path}`),
|
||||
readStdin: vi.fn(async () => 'stdin content'),
|
||||
now: () => 10,
|
||||
|
|
@ -171,7 +171,7 @@ describe('runKtxTextIngest', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
connectionId: 'warehouse',
|
||||
|
|
@ -180,7 +180,7 @@ describe('runKtxTextIngest', () => {
|
|||
assistantMessage: 'file:/tmp/docs/revenue.md',
|
||||
}),
|
||||
);
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
connectionId: 'warehouse',
|
||||
|
|
@ -194,9 +194,9 @@ describe('runKtxTextIngest', () => {
|
|||
expect(io.stdout()).toContain('stdin');
|
||||
});
|
||||
|
||||
it('uses bounded inline text previews as labels in plain output and capture metadata', async () => {
|
||||
it('uses bounded inline text previews as labels in plain output and ingest metadata', async () => {
|
||||
const io = makeIo();
|
||||
const capture = fakeCapture();
|
||||
const ingest = fakeIngest();
|
||||
const longText = `This inline note is intentionally long ${'x'.repeat(120)}`;
|
||||
|
||||
await expect(
|
||||
|
|
@ -212,7 +212,7 @@ describe('runKtxTextIngest', () => {
|
|||
io.io,
|
||||
{
|
||||
loadProject: vi.fn(async () => fakeProject()),
|
||||
createMemoryCapture: vi.fn(() => capture),
|
||||
createMemoryIngest: vi.fn(() => ingest),
|
||||
now: () => 10,
|
||||
},
|
||||
),
|
||||
|
|
@ -225,19 +225,19 @@ describe('runKtxTextIngest', () => {
|
|||
expect(output).not.toContain('text-1');
|
||||
expect(output).not.toContain(longText);
|
||||
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
userMessage: 'Ingest external text artifact "remember to call me Andrey" into KTX memory.',
|
||||
}),
|
||||
);
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
userMessage: 'Ingest external text artifact "first line second line" into KTX memory.',
|
||||
}),
|
||||
);
|
||||
expect(capture.capture).toHaveBeenNthCalledWith(
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
userMessage: 'Ingest external text artifact "This inline note is intentionally long xxxxxxxx..." into KTX memory.',
|
||||
|
|
@ -247,7 +247,7 @@ describe('runKtxTextIngest', () => {
|
|||
|
||||
it('continues after an item failure by default and stops when failFast is set', async () => {
|
||||
const continueIo = makeIo();
|
||||
const continueCapture = fakeCapture({ failRunIds: new Set(['run-1']) });
|
||||
const continueIngest = fakeIngest({ failRunIds: new Set(['run-1']) });
|
||||
|
||||
await expect(
|
||||
runKtxTextIngest(
|
||||
|
|
@ -262,12 +262,12 @@ describe('runKtxTextIngest', () => {
|
|||
continueIo.io,
|
||||
{
|
||||
loadProject: vi.fn(async () => fakeProject()),
|
||||
createMemoryCapture: vi.fn(() => continueCapture),
|
||||
createMemoryIngest: vi.fn(() => continueIngest),
|
||||
},
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(continueCapture.capture).toHaveBeenCalledTimes(2);
|
||||
expect(continueIngest.ingest).toHaveBeenCalledTimes(2);
|
||||
expect(JSON.parse(continueIo.stdout())).toMatchObject({
|
||||
status: 'failed',
|
||||
results: [
|
||||
|
|
@ -277,7 +277,7 @@ describe('runKtxTextIngest', () => {
|
|||
});
|
||||
|
||||
const failFastIo = makeIo();
|
||||
const failFastCapture = fakeCapture({ failRunIds: new Set(['run-1']) });
|
||||
const failFastIngest = fakeIngest({ failRunIds: new Set(['run-1']) });
|
||||
|
||||
await expect(
|
||||
runKtxTextIngest(
|
||||
|
|
@ -292,12 +292,12 @@ describe('runKtxTextIngest', () => {
|
|||
failFastIo.io,
|
||||
{
|
||||
loadProject: vi.fn(async () => fakeProject()),
|
||||
createMemoryCapture: vi.fn(() => failFastCapture),
|
||||
createMemoryIngest: vi.fn(() => failFastIngest),
|
||||
},
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(failFastCapture.capture).toHaveBeenCalledTimes(1);
|
||||
expect(failFastIngest.ingest).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(failFastIo.stdout()).results).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ describe('runKtxTextIngest', () => {
|
|||
failFast: false,
|
||||
},
|
||||
noInputIo.io,
|
||||
{ loadProject: vi.fn(), createMemoryCapture: vi.fn() },
|
||||
{ loadProject: vi.fn(), createMemoryIngest: vi.fn() },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
expect(noInputIo.stderr()).toContain('Provide at least one text item');
|
||||
|
|
@ -331,7 +331,7 @@ describe('runKtxTextIngest', () => {
|
|||
failFast: false,
|
||||
},
|
||||
emptyIo.io,
|
||||
{ loadProject: vi.fn(), createMemoryCapture: vi.fn() },
|
||||
{ loadProject: vi.fn(), createMemoryIngest: vi.fn() },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
expect(emptyIo.stderr()).toContain('Text item "text-1" is empty');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { readFile as fsReadFile } from 'node:fs/promises';
|
||||
import { basename, resolve } from 'node:path';
|
||||
import { createLocalProjectMemoryCapture, type MemoryAgentInput, type MemoryCaptureStatus } from '@ktx/context/memory';
|
||||
import { createLocalProjectMemoryIngest, type MemoryAgentInput, type MemoryIngestStatus } from '@ktx/context/memory';
|
||||
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { createRepainter, initViewState, renderContextBuildView, type ContextBuildTargetState } from './context-build-view.js';
|
||||
|
|
@ -17,10 +17,10 @@ export interface KtxTextIngestArgs {
|
|||
failFast: boolean;
|
||||
}
|
||||
|
||||
export interface TextMemoryCapturePort {
|
||||
capture(input: MemoryAgentInput): Promise<{ runId: string }>;
|
||||
export interface TextMemoryIngestPort {
|
||||
ingest(input: MemoryAgentInput): Promise<{ runId: string }>;
|
||||
waitForRun(runId: string): Promise<void>;
|
||||
status(runId: string): Promise<MemoryCaptureStatus | null>;
|
||||
status(runId: string): Promise<MemoryIngestStatus | null>;
|
||||
}
|
||||
|
||||
interface TextIngestItem {
|
||||
|
|
@ -32,14 +32,14 @@ interface TextIngestResult {
|
|||
label: string;
|
||||
runId: string | null;
|
||||
status: 'done' | 'error';
|
||||
captured: MemoryCaptureStatus['captured'];
|
||||
captured: MemoryIngestStatus['captured'];
|
||||
commitHash: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface KtxTextIngestDeps {
|
||||
loadProject?: (options: { projectDir: string }) => Promise<KtxLocalProject>;
|
||||
createMemoryCapture?: (project: KtxLocalProject) => TextMemoryCapturePort;
|
||||
createMemoryIngest?: (project: KtxLocalProject) => TextMemoryIngestPort;
|
||||
readFile?: (path: string) => Promise<string>;
|
||||
readStdin?: () => Promise<string>;
|
||||
now?: () => number;
|
||||
|
|
@ -48,8 +48,8 @@ export interface KtxTextIngestDeps {
|
|||
const INLINE_TEXT_LABEL_MAX_LENGTH = 50;
|
||||
const ANSI_ESCAPE_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
||||
|
||||
function defaultCreateMemoryCapture(project: KtxLocalProject): TextMemoryCapturePort {
|
||||
return createLocalProjectMemoryCapture(project);
|
||||
function defaultCreateMemoryIngest(project: KtxLocalProject): TextMemoryIngestPort {
|
||||
return createLocalProjectMemoryIngest(project);
|
||||
}
|
||||
|
||||
async function defaultReadStdin(): Promise<string> {
|
||||
|
|
@ -65,7 +65,7 @@ async function defaultReadFile(path: string): Promise<string> {
|
|||
return await fsReadFile(path, 'utf-8');
|
||||
}
|
||||
|
||||
function emptyCaptured(): MemoryCaptureStatus['captured'] {
|
||||
function emptyCaptured(): MemoryIngestStatus['captured'] {
|
||||
return { wiki: [], sl: [], xrefs: [] };
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ function renderTextIngestView(state: ReturnType<typeof initViewState>, styled: b
|
|||
});
|
||||
}
|
||||
|
||||
function summarizeCaptured(captured: MemoryCaptureStatus['captured']): string {
|
||||
function summarizeCaptured(captured: MemoryIngestStatus['captured']): string {
|
||||
const parts = [
|
||||
`wiki=${captured.wiki.length}`,
|
||||
`sl=${captured.sl.length}`,
|
||||
|
|
@ -191,7 +191,7 @@ function summarizeCaptured(captured: MemoryCaptureStatus['captured']): string {
|
|||
return parts.join(', ');
|
||||
}
|
||||
|
||||
function resultFromStatus(label: string, status: MemoryCaptureStatus): TextIngestResult {
|
||||
function resultFromStatus(label: string, status: MemoryIngestStatus): TextIngestResult {
|
||||
return {
|
||||
label,
|
||||
runId: status.runId,
|
||||
|
|
@ -251,7 +251,7 @@ export async function runKtxTextIngest(
|
|||
}
|
||||
|
||||
const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: args.projectDir });
|
||||
const memoryCapture = (deps.createMemoryCapture ?? defaultCreateMemoryCapture)(project);
|
||||
const memoryIngest = (deps.createMemoryIngest ?? defaultCreateMemoryIngest)(project);
|
||||
const now = deps.now ?? (() => Date.now());
|
||||
const batchId = now();
|
||||
const state = initViewState(items.map((item) => makeTarget(item.label)));
|
||||
|
|
@ -292,7 +292,7 @@ export async function runKtxTextIngest(
|
|||
let runId: string | null = null;
|
||||
let result: TextIngestResult;
|
||||
try {
|
||||
const captureInput: MemoryAgentInput = {
|
||||
const ingestInput: MemoryAgentInput = {
|
||||
userId: args.userId,
|
||||
chatId: `cli-text-ingest-${batchId}-${index + 1}`,
|
||||
userMessage: `Ingest external text artifact ${artifactReference(item.label)} into KTX memory.`,
|
||||
|
|
@ -300,12 +300,12 @@ export async function runKtxTextIngest(
|
|||
...(args.connectionId ? { connectionId: args.connectionId } : {}),
|
||||
sourceType: 'external_ingest',
|
||||
};
|
||||
const capture = await memoryCapture.capture(captureInput);
|
||||
runId = capture.runId;
|
||||
await memoryCapture.waitForRun(runId);
|
||||
const status = await memoryCapture.status(runId);
|
||||
const ingest = await memoryIngest.ingest(ingestInput);
|
||||
runId = ingest.runId;
|
||||
await memoryIngest.waitForRun(runId);
|
||||
const status = await memoryIngest.status(runId);
|
||||
if (!status) {
|
||||
throw new Error(`Memory capture run "${runId}" was not found.`);
|
||||
throw new Error(`Memory ingest run "${runId}" was not found.`);
|
||||
}
|
||||
result = resultFromStatus(item.label, status);
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue