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.
Files changed (42) hide show
  1. package/README.md +127 -140
  2. package/package.json +6 -3
  3. package/src/cli.ts +54 -369
  4. package/src/commands/init.ts +15 -29
  5. package/src/db/memory-db.test.ts +45 -0
  6. package/src/db/memory-db.ts +47 -21
  7. package/src/db/sqlite-setup.ts +26 -3
  8. package/src/db/workflow-db.ts +12 -5
  9. package/src/parser/config-schema.ts +17 -13
  10. package/src/parser/schema.ts +4 -2
  11. package/src/runner/__test__/llm-mock-setup.ts +173 -0
  12. package/src/runner/__test__/llm-test-setup.ts +271 -0
  13. package/src/runner/engine-executor.test.ts +25 -18
  14. package/src/runner/executors/blueprint-executor.ts +0 -1
  15. package/src/runner/executors/dynamic-executor.ts +11 -6
  16. package/src/runner/executors/engine-executor.ts +5 -1
  17. package/src/runner/executors/llm-executor.ts +502 -1033
  18. package/src/runner/executors/memory-executor.ts +35 -19
  19. package/src/runner/executors/plan-executor.ts +0 -1
  20. package/src/runner/executors/types.ts +4 -4
  21. package/src/runner/llm-adapter.integration.test.ts +151 -0
  22. package/src/runner/llm-adapter.ts +270 -1398
  23. package/src/runner/llm-clarification.test.ts +91 -106
  24. package/src/runner/llm-executor.test.ts +217 -1181
  25. package/src/runner/memoization.test.ts +0 -1
  26. package/src/runner/recovery-security.test.ts +51 -20
  27. package/src/runner/reflexion.test.ts +55 -18
  28. package/src/runner/standard-tools-integration.test.ts +137 -87
  29. package/src/runner/step-executor.test.ts +36 -80
  30. package/src/runner/step-executor.ts +0 -2
  31. package/src/runner/test-harness.ts +3 -29
  32. package/src/runner/tool-integration.test.ts +122 -73
  33. package/src/runner/workflow-runner.ts +110 -49
  34. package/src/runner/workflow-scheduler.ts +11 -1
  35. package/src/runner/workflow-summary.ts +144 -0
  36. package/src/utils/auth-manager.test.ts +10 -520
  37. package/src/utils/auth-manager.ts +3 -756
  38. package/src/utils/config-loader.ts +12 -0
  39. package/src/utils/constants.ts +0 -17
  40. package/src/utils/process-sandbox.ts +15 -3
  41. package/src/runner/llm-adapter-runtime.test.ts +0 -209
  42. package/src/runner/llm-adapter.test.ts +0 -1012
@@ -1,12 +1,15 @@
1
- import type { MemoryDb } from '../../db/memory-db.ts';
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 (setting/deleting keys in memory)
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 memoryDb = options.memoryDb;
22
- if (!memoryDb) {
23
- throw new Error('Memory database not initialized');
24
+ const memoryDbFromOptions = options.memoryDb;
25
+ if (!memoryDbFromOptions && !options.memoryDb) {
26
+ // We'll initialize it below if not provided
24
27
  }
25
28
 
26
- const requestedModel = step.model || 'local';
27
- if (!requestedModel.toLowerCase().startsWith('local')) {
28
- throw new Error(`Memory steps only support local embeddings (requested: ${requestedModel})`);
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
- const adapterResult = options.getAdapter ? options.getAdapter(requestedModel) : null;
32
- const adapter = adapterResult?.adapter;
33
- const resolvedModel = adapterResult?.resolvedModel ?? requestedModel;
34
- if (!resolvedModel.toLowerCase().startsWith('local')) {
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 adapter.embed(text, undefined, { signal: abortSignal });
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 adapter.embed(query, undefined, { signal: abortSignal });
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
- import type { getAdapter } from '../llm-adapter.ts';
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
- getAdapter?: typeof getAdapter;
66
- executeStep?: any; // To avoid circular dependency
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
+ });