keystone-cli 1.3.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -140
- package/package.json +6 -3
- package/src/cli.ts +54 -369
- package/src/commands/init.ts +15 -29
- package/src/db/memory-db.test.ts +45 -0
- package/src/db/memory-db.ts +47 -21
- package/src/db/sqlite-setup.ts +26 -3
- package/src/db/workflow-db.ts +12 -5
- package/src/parser/config-schema.ts +17 -13
- package/src/parser/schema.ts +4 -2
- package/src/runner/__test__/llm-mock-setup.ts +173 -0
- package/src/runner/__test__/llm-test-setup.ts +271 -0
- package/src/runner/engine-executor.test.ts +25 -18
- package/src/runner/executors/blueprint-executor.ts +0 -1
- package/src/runner/executors/dynamic-executor.ts +11 -6
- package/src/runner/executors/engine-executor.ts +5 -1
- package/src/runner/executors/llm-executor.ts +502 -1033
- package/src/runner/executors/memory-executor.ts +35 -19
- package/src/runner/executors/plan-executor.ts +0 -1
- package/src/runner/executors/types.ts +4 -4
- package/src/runner/llm-adapter.integration.test.ts +151 -0
- package/src/runner/llm-adapter.ts +270 -1398
- package/src/runner/llm-clarification.test.ts +91 -106
- package/src/runner/llm-executor.test.ts +217 -1181
- package/src/runner/memoization.test.ts +0 -1
- package/src/runner/recovery-security.test.ts +51 -20
- package/src/runner/reflexion.test.ts +55 -18
- package/src/runner/standard-tools-integration.test.ts +137 -87
- package/src/runner/step-executor.test.ts +36 -80
- package/src/runner/step-executor.ts +0 -2
- package/src/runner/test-harness.ts +3 -29
- package/src/runner/tool-integration.test.ts +122 -73
- package/src/runner/workflow-runner.ts +110 -49
- package/src/runner/workflow-scheduler.ts +11 -1
- package/src/runner/workflow-summary.ts +144 -0
- package/src/utils/auth-manager.test.ts +10 -520
- package/src/utils/auth-manager.ts +3 -756
- package/src/utils/config-loader.ts +12 -0
- package/src/utils/constants.ts +0 -17
- package/src/utils/process-sandbox.ts +15 -3
- package/src/runner/llm-adapter-runtime.test.ts +0 -209
- package/src/runner/llm-adapter.test.ts +0 -1012
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { embed } from 'ai';
|
|
2
|
+
import { MemoryDb } from '../../db/memory-db.ts';
|
|
2
3
|
import type { ExpressionContext } from '../../expression/evaluator.ts';
|
|
3
4
|
import { ExpressionEvaluator } from '../../expression/evaluator.ts';
|
|
4
5
|
import type { MemoryStep } from '../../parser/schema.ts';
|
|
6
|
+
import { ConfigLoader } from '../../utils/config-loader.ts';
|
|
5
7
|
import type { Logger } from '../../utils/logger.ts';
|
|
8
|
+
import { getEmbeddingModel } from '../llm-adapter.ts';
|
|
6
9
|
import type { StepExecutorOptions, StepResult } from './types.ts';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
|
-
* Execute a memory step (
|
|
12
|
+
* Execute a memory step (storing/searching embeddings in memory)
|
|
10
13
|
*/
|
|
11
14
|
export async function executeMemoryStep(
|
|
12
15
|
step: MemoryStep,
|
|
@@ -18,32 +21,45 @@ export async function executeMemoryStep(
|
|
|
18
21
|
if (abortSignal?.aborted) {
|
|
19
22
|
throw new Error('Memory operation aborted');
|
|
20
23
|
}
|
|
21
|
-
const
|
|
22
|
-
if (!memoryDb) {
|
|
23
|
-
|
|
24
|
+
const memoryDbFromOptions = options.memoryDb;
|
|
25
|
+
if (!memoryDbFromOptions && !options.memoryDb) {
|
|
26
|
+
// We'll initialize it below if not provided
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
29
|
+
// Get embedding model and dimension from config or step
|
|
30
|
+
const config = ConfigLoader.load();
|
|
31
|
+
const modelName = step.model || config.embedding_model;
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error(`Memory steps only support local embeddings (requested: ${resolvedModel})`);
|
|
36
|
-
}
|
|
37
|
-
if (!adapter || !adapter.embed) {
|
|
38
|
-
throw new Error(`Model ${resolvedModel} does not support embeddings`);
|
|
33
|
+
if (!modelName) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'No embedding model configured. Set embedding_model in config or specify model in step.'
|
|
36
|
+
);
|
|
39
37
|
}
|
|
40
38
|
|
|
39
|
+
// Resolve provider dimension
|
|
40
|
+
const providerName = ConfigLoader.getProviderForModel(modelName);
|
|
41
|
+
const providerConfig = config.providers[providerName];
|
|
42
|
+
const dimension = providerConfig?.embedding_dimension || config.embedding_dimension || 384;
|
|
43
|
+
|
|
44
|
+
const memoryDb = memoryDbFromOptions || new MemoryDb('.keystone/memory.db', dimension);
|
|
45
|
+
|
|
46
|
+
// Helper to get embedding using AI SDK
|
|
47
|
+
const getEmbedding = async (text: string): Promise<number[]> => {
|
|
48
|
+
const model = await getEmbeddingModel(modelName);
|
|
49
|
+
const result = await embed({
|
|
50
|
+
model,
|
|
51
|
+
value: text,
|
|
52
|
+
abortSignal,
|
|
53
|
+
});
|
|
54
|
+
return result.embedding;
|
|
55
|
+
};
|
|
56
|
+
|
|
41
57
|
switch (step.op) {
|
|
42
58
|
case 'store': {
|
|
43
59
|
const text = ExpressionEvaluator.evaluateString(step.text || '', context);
|
|
44
60
|
if (!text) throw new Error('Text is required for memory store operation');
|
|
45
61
|
|
|
46
|
-
const embedding = await
|
|
62
|
+
const embedding = await getEmbedding(text);
|
|
47
63
|
const metadata = step.metadata || {};
|
|
48
64
|
const id = await memoryDb.store(text, embedding, metadata as Record<string, unknown>);
|
|
49
65
|
|
|
@@ -56,7 +72,7 @@ export async function executeMemoryStep(
|
|
|
56
72
|
const query = ExpressionEvaluator.evaluateString(step.query || '', context);
|
|
57
73
|
if (!query) throw new Error('Query is required for memory search operation');
|
|
58
74
|
|
|
59
|
-
const embedding = await
|
|
75
|
+
const embedding = await getEmbedding(query);
|
|
60
76
|
const limit = step.limit || 5;
|
|
61
77
|
const results = await memoryDb.search(embedding, limit);
|
|
62
78
|
|
|
@@ -97,7 +97,6 @@ export async function executePlanStep(
|
|
|
97
97
|
options.mcpManager,
|
|
98
98
|
options.artifactRoot, // Note: using artifactRoot as fallback for workflowDir if not explicit
|
|
99
99
|
options.abortSignal,
|
|
100
|
-
options.getAdapter,
|
|
101
100
|
options.emitEvent,
|
|
102
101
|
options.runId ? { runId: options.runId } : undefined
|
|
103
102
|
);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { MemoryDb } from '../../db/memory-db.ts';
|
|
2
2
|
import type { WorkflowDb } from '../../db/workflow-db.ts';
|
|
3
3
|
import type { ExpressionContext } from '../../expression/evaluator.ts';
|
|
4
|
-
import type { WorkflowStep } from '../../parser/schema.ts';
|
|
4
|
+
import type { Step, WorkflowStep } from '../../parser/schema.ts';
|
|
5
5
|
import type { Logger } from '../../utils/logger.ts';
|
|
6
6
|
import type { SafeSandbox } from '../../utils/sandbox.ts';
|
|
7
7
|
import type { WorkflowEvent } from '../events.ts';
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
import type { MCPManager } from '../mcp-manager.ts';
|
|
10
10
|
import type { executeLlmStep } from './llm-executor.ts';
|
|
11
11
|
|
|
@@ -62,8 +62,8 @@ export interface StepExecutorOptions {
|
|
|
62
62
|
debug?: boolean;
|
|
63
63
|
allowInsecure?: boolean;
|
|
64
64
|
emitEvent?: (event: WorkflowEvent) => void;
|
|
65
|
-
|
|
66
|
-
executeStep?:
|
|
65
|
+
|
|
66
|
+
executeStep?: (step: Step, context: ExpressionContext) => Promise<StepResult>; // To avoid circular dependency
|
|
67
67
|
executeLlmStep?: typeof executeLlmStep;
|
|
68
68
|
sandbox?: typeof SafeSandbox;
|
|
69
69
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
|
2
|
+
import { AuthManager } from '../utils/auth-manager';
|
|
3
|
+
import { ConfigLoader } from '../utils/config-loader';
|
|
4
|
+
import { resetLlmMocks, setupLlmMocks } from './__test__/llm-test-setup';
|
|
5
|
+
import {
|
|
6
|
+
DynamicProviderRegistry,
|
|
7
|
+
getEmbeddingModel,
|
|
8
|
+
getModel,
|
|
9
|
+
resetProviderRegistry,
|
|
10
|
+
} from './llm-adapter';
|
|
11
|
+
|
|
12
|
+
// Mocks for AI SDK models
|
|
13
|
+
const mockLanguageModel = {
|
|
14
|
+
specificationVersion: 'v1',
|
|
15
|
+
provider: 'test-provider',
|
|
16
|
+
modelId: 'test-model',
|
|
17
|
+
doGenerate: async () => ({}),
|
|
18
|
+
doStream: async () => ({}),
|
|
19
|
+
} as any;
|
|
20
|
+
|
|
21
|
+
const mockEmbeddingModel = {
|
|
22
|
+
specificationVersion: 'v1',
|
|
23
|
+
provider: 'test-provider',
|
|
24
|
+
modelId: 'test-embedding-model',
|
|
25
|
+
doEmbed: async () => ({}),
|
|
26
|
+
doEmbedMany: async () => ({}),
|
|
27
|
+
} as any;
|
|
28
|
+
|
|
29
|
+
describe('LLM Adapter (AI SDK)', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
setupLlmMocks();
|
|
32
|
+
ConfigLoader.clear();
|
|
33
|
+
resetProviderRegistry();
|
|
34
|
+
// Reset AuthManager mocks if any
|
|
35
|
+
mock.restore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
resetLlmMocks();
|
|
40
|
+
mock.restore();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('getModel', () => {
|
|
44
|
+
it('should load a provider and return a language model', async () => {
|
|
45
|
+
// Mock Config
|
|
46
|
+
ConfigLoader.setConfig({
|
|
47
|
+
default_provider: 'test-provider',
|
|
48
|
+
providers: {
|
|
49
|
+
'test-provider': {
|
|
50
|
+
type: 'openai', // Use standard type to trigger logic
|
|
51
|
+
package: '@ai-sdk/openai', // Use real package name to match mock
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
model_mappings: {},
|
|
55
|
+
} as any);
|
|
56
|
+
|
|
57
|
+
// With shared setupLlmMocks, we expect 'mock' provider
|
|
58
|
+
const model = (await getModel('model-name')) as any;
|
|
59
|
+
expect(model.modelId).toBe('mock-model');
|
|
60
|
+
expect(model.provider).toBe('mock');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle auth token retrieval for standard providers', async () => {
|
|
64
|
+
ConfigLoader.setConfig({
|
|
65
|
+
default_provider: 'openai',
|
|
66
|
+
providers: {
|
|
67
|
+
openai: {
|
|
68
|
+
type: 'openai',
|
|
69
|
+
package: '@ai-sdk/openai',
|
|
70
|
+
api_key_env: 'OPENAI_API_KEY',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
model_mappings: {},
|
|
74
|
+
} as any);
|
|
75
|
+
|
|
76
|
+
spyOn(ConfigLoader, 'getSecret').mockReturnValue('fake-token');
|
|
77
|
+
|
|
78
|
+
const model = (await getModel('gpt-4')) as any;
|
|
79
|
+
// With global mock, we mostly check it didn't throw and loaded the 'mock' provider
|
|
80
|
+
expect(model.provider).toBe('mock');
|
|
81
|
+
expect(ConfigLoader.getSecret).toHaveBeenCalledWith('OPENAI_API_KEY');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('getEmbeddingModel', () => {
|
|
86
|
+
it('should return an embedding model if supported by provider', async () => {
|
|
87
|
+
ConfigLoader.setConfig({
|
|
88
|
+
default_provider: 'embed-provider',
|
|
89
|
+
providers: {
|
|
90
|
+
'embed-provider': { type: 'custom', package: 'pkg' },
|
|
91
|
+
},
|
|
92
|
+
model_mappings: {},
|
|
93
|
+
} as any);
|
|
94
|
+
|
|
95
|
+
const mockProvider = (modelId: string) => mockLanguageModel;
|
|
96
|
+
mockProvider.textEmbeddingModel = (modelId: string) => mockEmbeddingModel;
|
|
97
|
+
|
|
98
|
+
spyOn(DynamicProviderRegistry, 'getProvider').mockResolvedValue(() => mockProvider);
|
|
99
|
+
|
|
100
|
+
const model = (await getEmbeddingModel('text-embedding-3')) as any;
|
|
101
|
+
expect(model.modelId).toBe(mockEmbeddingModel.modelId);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should throw if provider does not support embeddings', async () => {
|
|
105
|
+
ConfigLoader.setConfig({
|
|
106
|
+
default_provider: 'bad-provider',
|
|
107
|
+
providers: {
|
|
108
|
+
'bad-provider': { type: 'custom', package: 'pkg' },
|
|
109
|
+
},
|
|
110
|
+
model_mappings: {},
|
|
111
|
+
} as any);
|
|
112
|
+
|
|
113
|
+
const mockProvider = (modelId: string) => mockLanguageModel;
|
|
114
|
+
// No textEmbeddingModel method
|
|
115
|
+
|
|
116
|
+
spyOn(DynamicProviderRegistry, 'getProvider').mockResolvedValue(() => mockProvider);
|
|
117
|
+
|
|
118
|
+
// Use a non-default model name to avoid fallback to LocalEmbeddingModel
|
|
119
|
+
await expect(getEmbeddingModel('non-default-model')).rejects.toThrow(
|
|
120
|
+
/does not support embeddings/
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('Tool Cases', () => {
|
|
126
|
+
it('should handle assistant response with tool calls and NO content', async () => {
|
|
127
|
+
ConfigLoader.setConfig({
|
|
128
|
+
default_provider: 'test-provider',
|
|
129
|
+
providers: { 'test-provider': { type: 'openai', package: 'test-pkg' } },
|
|
130
|
+
model_mappings: {},
|
|
131
|
+
} as any);
|
|
132
|
+
|
|
133
|
+
const mockProvider = (modelId: string) => ({
|
|
134
|
+
...mockLanguageModel,
|
|
135
|
+
doGenerate: async () => ({
|
|
136
|
+
content: [
|
|
137
|
+
{ type: 'tool-call', toolCallId: '1', toolName: 'test', args: {}, input: '{}' },
|
|
138
|
+
],
|
|
139
|
+
finishReason: 'tool-calls',
|
|
140
|
+
usage: { promptTokens: 1, completionTokens: 1 },
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
spyOn(DynamicProviderRegistry, 'getProvider').mockResolvedValue(() => mockProvider);
|
|
144
|
+
|
|
145
|
+
const model = (await getModel('model')) as any;
|
|
146
|
+
const result = await model.doGenerate({ input: [], prompt: [], mode: { type: 'regular' } });
|
|
147
|
+
expect(result.content).toHaveLength(1);
|
|
148
|
+
expect(result.content[0].type).toBe('tool-call');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|