confused-ai-core 0.1.0

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 (114) hide show
  1. package/FEATURES.md +169 -0
  2. package/package.json +119 -0
  3. package/src/agent.ts +187 -0
  4. package/src/agentic/index.ts +87 -0
  5. package/src/agentic/runner.ts +386 -0
  6. package/src/agentic/types.ts +91 -0
  7. package/src/artifacts/artifact.ts +417 -0
  8. package/src/artifacts/index.ts +42 -0
  9. package/src/artifacts/media.ts +304 -0
  10. package/src/cli/index.ts +122 -0
  11. package/src/core/base-agent.ts +151 -0
  12. package/src/core/context-builder.ts +106 -0
  13. package/src/core/index.ts +8 -0
  14. package/src/core/schemas.ts +17 -0
  15. package/src/core/types.ts +158 -0
  16. package/src/create-agent.ts +309 -0
  17. package/src/debug-logger.ts +188 -0
  18. package/src/dx/agent.ts +88 -0
  19. package/src/dx/define-agent.ts +183 -0
  20. package/src/dx/dev-logger.ts +57 -0
  21. package/src/dx/index.ts +11 -0
  22. package/src/errors.ts +175 -0
  23. package/src/execution/engine.ts +522 -0
  24. package/src/execution/graph-builder.ts +362 -0
  25. package/src/execution/index.ts +8 -0
  26. package/src/execution/types.ts +257 -0
  27. package/src/execution/worker-pool.ts +308 -0
  28. package/src/extensions/index.ts +123 -0
  29. package/src/guardrails/allowlist.ts +155 -0
  30. package/src/guardrails/index.ts +17 -0
  31. package/src/guardrails/types.ts +159 -0
  32. package/src/guardrails/validator.ts +265 -0
  33. package/src/index.ts +74 -0
  34. package/src/knowledge/index.ts +5 -0
  35. package/src/knowledge/types.ts +52 -0
  36. package/src/learning/in-memory-store.ts +72 -0
  37. package/src/learning/index.ts +6 -0
  38. package/src/learning/types.ts +42 -0
  39. package/src/llm/cache.ts +300 -0
  40. package/src/llm/index.ts +22 -0
  41. package/src/llm/model-resolver.ts +81 -0
  42. package/src/llm/openai-provider.ts +313 -0
  43. package/src/llm/openrouter-provider.ts +29 -0
  44. package/src/llm/types.ts +131 -0
  45. package/src/memory/in-memory-store.ts +255 -0
  46. package/src/memory/index.ts +7 -0
  47. package/src/memory/types.ts +193 -0
  48. package/src/memory/vector-store.ts +251 -0
  49. package/src/observability/console-logger.ts +123 -0
  50. package/src/observability/index.ts +12 -0
  51. package/src/observability/metrics.ts +85 -0
  52. package/src/observability/otlp-exporter.ts +417 -0
  53. package/src/observability/tracer.ts +105 -0
  54. package/src/observability/types.ts +341 -0
  55. package/src/orchestration/agent-adapter.ts +33 -0
  56. package/src/orchestration/index.ts +34 -0
  57. package/src/orchestration/load-balancer.ts +151 -0
  58. package/src/orchestration/mcp-types.ts +59 -0
  59. package/src/orchestration/message-bus.ts +192 -0
  60. package/src/orchestration/orchestrator.ts +349 -0
  61. package/src/orchestration/pipeline.ts +66 -0
  62. package/src/orchestration/supervisor.ts +107 -0
  63. package/src/orchestration/swarm.ts +1099 -0
  64. package/src/orchestration/toolkit.ts +47 -0
  65. package/src/orchestration/types.ts +339 -0
  66. package/src/planner/classical-planner.ts +383 -0
  67. package/src/planner/index.ts +8 -0
  68. package/src/planner/llm-planner.ts +353 -0
  69. package/src/planner/types.ts +227 -0
  70. package/src/planner/validator.ts +297 -0
  71. package/src/production/circuit-breaker.ts +290 -0
  72. package/src/production/graceful-shutdown.ts +251 -0
  73. package/src/production/health.ts +333 -0
  74. package/src/production/index.ts +57 -0
  75. package/src/production/latency-eval.ts +62 -0
  76. package/src/production/rate-limiter.ts +287 -0
  77. package/src/production/resumable-stream.ts +289 -0
  78. package/src/production/types.ts +81 -0
  79. package/src/sdk/index.ts +374 -0
  80. package/src/session/db-driver.ts +50 -0
  81. package/src/session/in-memory-store.ts +235 -0
  82. package/src/session/index.ts +12 -0
  83. package/src/session/sql-store.ts +315 -0
  84. package/src/session/sqlite-store.ts +61 -0
  85. package/src/session/types.ts +153 -0
  86. package/src/tools/base-tool.ts +223 -0
  87. package/src/tools/browser-tool.ts +123 -0
  88. package/src/tools/calculator-tool.ts +265 -0
  89. package/src/tools/file-tools.ts +394 -0
  90. package/src/tools/github-tool.ts +432 -0
  91. package/src/tools/hackernews-tool.ts +187 -0
  92. package/src/tools/http-tool.ts +118 -0
  93. package/src/tools/index.ts +99 -0
  94. package/src/tools/jira-tool.ts +373 -0
  95. package/src/tools/notion-tool.ts +322 -0
  96. package/src/tools/openai-tool.ts +236 -0
  97. package/src/tools/registry.ts +131 -0
  98. package/src/tools/serpapi-tool.ts +234 -0
  99. package/src/tools/shell-tool.ts +118 -0
  100. package/src/tools/slack-tool.ts +327 -0
  101. package/src/tools/telegram-tool.ts +127 -0
  102. package/src/tools/types.ts +229 -0
  103. package/src/tools/websearch-tool.ts +335 -0
  104. package/src/tools/wikipedia-tool.ts +177 -0
  105. package/src/tools/yfinance-tool.ts +33 -0
  106. package/src/voice/index.ts +17 -0
  107. package/src/voice/voice-provider.ts +228 -0
  108. package/tests/artifact.test.ts +241 -0
  109. package/tests/circuit-breaker.test.ts +171 -0
  110. package/tests/health.test.ts +192 -0
  111. package/tests/llm-cache.test.ts +186 -0
  112. package/tests/rate-limiter.test.ts +161 -0
  113. package/tsconfig.json +29 -0
  114. package/vitest.config.ts +47 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Wikipedia tool implementation - TypeScript WikipediaTools
3
+ */
4
+
5
+ import { z } from 'zod';
6
+ import { BaseTool, BaseToolConfig } from './base-tool.js';
7
+ import { ToolContext, ToolCategory } from './types.js';
8
+
9
+ /**
10
+ * Wikipedia API response types
11
+ */
12
+ interface WikipediaSummaryResponse {
13
+ title?: string;
14
+ extract?: string;
15
+ content_urls?: {
16
+ desktop?: {
17
+ page?: string;
18
+ };
19
+ };
20
+ }
21
+
22
+ interface WikipediaSearchResult {
23
+ title: string;
24
+ snippet: string;
25
+ }
26
+
27
+ interface WikipediaSearchResponse {
28
+ query?: {
29
+ search?: WikipediaSearchResult[];
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Wikipedia search result
35
+ */
36
+ interface WikipediaResult {
37
+ title: string;
38
+ content: string;
39
+ url: string;
40
+ error?: string;
41
+ }
42
+
43
+ /**
44
+ * Parameters for Wikipedia search
45
+ */
46
+ const WikipediaSearchParameters = z.object({
47
+ query: z.string().describe('The topic or query to search for on Wikipedia'),
48
+ });
49
+
50
+ /**
51
+ * Wikipedia search tool
52
+ */
53
+ export class WikipediaSearchTool extends BaseTool<typeof WikipediaSearchParameters, WikipediaResult> {
54
+ constructor(config?: Partial<Omit<BaseToolConfig<typeof WikipediaSearchParameters>, 'parameters'>>) {
55
+ super({
56
+ name: config?.name ?? 'wikipedia.search',
57
+ description: config?.description ?? 'Search Wikipedia for a topic and get a summary',
58
+ parameters: WikipediaSearchParameters,
59
+ category: config?.category ?? ToolCategory.WEB,
60
+ permissions: {
61
+ allowNetwork: true,
62
+ maxExecutionTimeMs: 30000,
63
+ ...config?.permissions,
64
+ },
65
+ ...config,
66
+ });
67
+ }
68
+
69
+ protected async performExecute(
70
+ params: z.infer<typeof WikipediaSearchParameters>,
71
+ _context: ToolContext
72
+ ): Promise<WikipediaResult> {
73
+ try {
74
+ // Use Wikipedia's REST API
75
+ const searchUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(params.query.replace(/\s+/g, '_'))}`;
76
+
77
+ const response = await fetch(searchUrl, {
78
+ headers: {
79
+ 'User-Agent': 'AgentFramework/1.0 (https://github.com/agent-framework)',
80
+ },
81
+ });
82
+
83
+ if (!response.ok) {
84
+ // Try searching for the page first
85
+ return this.searchAndGetSummary(params.query);
86
+ }
87
+
88
+ const data = (await response.json()) as WikipediaSummaryResponse;
89
+
90
+ return {
91
+ title: data.title || params.query,
92
+ content: data.extract || 'No summary available',
93
+ url: data.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${encodeURIComponent(params.query.replace(/\s+/g, '_'))}`,
94
+ };
95
+ } catch (error) {
96
+ return {
97
+ title: params.query,
98
+ content: '',
99
+ url: '',
100
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
101
+ };
102
+ }
103
+ }
104
+
105
+ private async searchAndGetSummary(query: string): Promise<WikipediaResult> {
106
+ try {
107
+ // Search for the page
108
+ const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*`;
109
+
110
+ const searchResponse = await fetch(searchUrl, {
111
+ headers: {
112
+ 'User-Agent': 'AgentFramework/1.0 (https://github.com/agent-framework)',
113
+ },
114
+ });
115
+
116
+ if (!searchResponse.ok) {
117
+ throw new Error(`Wikipedia search failed: ${searchResponse.status}`);
118
+ }
119
+
120
+ const searchData = (await searchResponse.json()) as WikipediaSearchResponse;
121
+ const searchResults = searchData.query?.search || [];
122
+
123
+ if (searchResults.length === 0) {
124
+ return {
125
+ title: query,
126
+ content: 'No results found on Wikipedia',
127
+ url: '',
128
+ };
129
+ }
130
+
131
+ // Get the first result's title and fetch its summary
132
+ const firstResult = searchResults[0];
133
+ const pageTitle = firstResult.title;
134
+
135
+ const summaryUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(pageTitle.replace(/\s+/g, '_'))}`;
136
+
137
+ const summaryResponse = await fetch(summaryUrl, {
138
+ headers: {
139
+ 'User-Agent': 'AgentFramework/1.0 (https://github.com/agent-framework)',
140
+ },
141
+ });
142
+
143
+ if (!summaryResponse.ok) {
144
+ // Return the search snippet if summary fails
145
+ return {
146
+ title: pageTitle,
147
+ content: firstResult.snippet.replace(/<[^>]*>/g, ''),
148
+ url: `https://en.wikipedia.org/wiki/${encodeURIComponent(pageTitle.replace(/\s+/g, '_'))}`,
149
+ };
150
+ }
151
+
152
+ const summaryData = (await summaryResponse.json()) as WikipediaSummaryResponse;
153
+
154
+ return {
155
+ title: summaryData.title || pageTitle,
156
+ content: summaryData.extract || firstResult.snippet.replace(/<[^>]*>/g, ''),
157
+ url: summaryData.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${encodeURIComponent(pageTitle.replace(/\s+/g, '_'))}`,
158
+ };
159
+ } catch (error) {
160
+ return {
161
+ title: query,
162
+ content: '',
163
+ url: '',
164
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
165
+ };
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Wikipedia toolkit
172
+ */
173
+ export class WikipediaToolkit {
174
+ static create(): Array<WikipediaSearchTool> {
175
+ return [new WikipediaSearchTool()];
176
+ }
177
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ import yahooFinance from 'yahoo-finance2';
3
+ import { BaseTool } from './base-tool.js';
4
+ import { ToolCategory, ToolContext } from './types.js';
5
+
6
+ const YFinanceParameters = z.object({
7
+ symbol: z.string().describe('The stock symbol (e.g., AAPL)'),
8
+ });
9
+
10
+ type YFinanceParameters = z.infer<typeof YFinanceParameters>;
11
+
12
+ export class YFinanceTool extends BaseTool<typeof YFinanceParameters, unknown> {
13
+ constructor() {
14
+ super({
15
+ name: 'yfinance_stock',
16
+ description: 'Fetch real-time stock quote data using Yahoo Finance',
17
+ parameters: YFinanceParameters,
18
+ category: ToolCategory.WEB,
19
+ permissions: {
20
+ allowNetwork: true,
21
+ allowFileSystem: false,
22
+ maxExecutionTimeMs: 30000,
23
+ },
24
+ });
25
+ }
26
+
27
+ protected async performExecute(
28
+ params: YFinanceParameters,
29
+ _context: ToolContext
30
+ ): Promise<unknown> {
31
+ return await yahooFinance.quote(params.symbol);
32
+ }
33
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Voice Module Exports
3
+ */
4
+
5
+ export {
6
+ OpenAIVoiceProvider,
7
+ ElevenLabsVoiceProvider,
8
+ createVoiceProvider,
9
+ } from './voice-provider.js';
10
+
11
+ export type {
12
+ VoiceConfig,
13
+ VoiceProvider,
14
+ TTSResult,
15
+ STTResult,
16
+ OpenAIVoice,
17
+ } from './voice-provider.js';
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Voice Provider - VoltAgent-Style TTS/STT Support
3
+ *
4
+ * Text-to-speech and speech-to-text capabilities:
5
+ * - OpenAI voice support
6
+ * - ElevenLabs integration
7
+ * - Custom voice provider interface
8
+ */
9
+
10
+ /** Voice configuration */
11
+ export interface VoiceConfig {
12
+ /** Voice provider type */
13
+ readonly provider: 'openai' | 'elevenlabs' | 'custom';
14
+ /** Voice ID */
15
+ readonly voiceId?: string;
16
+ /** Model for TTS */
17
+ readonly model?: string;
18
+ /** Speed (0.25 to 4.0) */
19
+ readonly speed?: number;
20
+ /** API key (if not in env) */
21
+ readonly apiKey?: string;
22
+ }
23
+
24
+ /** TTS result */
25
+ export interface TTSResult {
26
+ /** Audio data as ArrayBuffer */
27
+ readonly audio: ArrayBuffer;
28
+ /** Audio format */
29
+ readonly format: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm';
30
+ /** Duration in seconds */
31
+ readonly durationSeconds?: number;
32
+ /** Character count */
33
+ readonly characterCount: number;
34
+ }
35
+
36
+ /** STT result */
37
+ export interface STTResult {
38
+ /** Transcribed text */
39
+ readonly text: string;
40
+ /** Language detected */
41
+ readonly language?: string;
42
+ /** Confidence score (0-1) */
43
+ readonly confidence?: number;
44
+ /** Duration of audio in seconds */
45
+ readonly durationSeconds?: number;
46
+ }
47
+
48
+ /** Voice provider interface */
49
+ export interface VoiceProvider {
50
+ /** Text-to-speech */
51
+ textToSpeech(text: string, options?: Partial<VoiceConfig>): Promise<TTSResult>;
52
+
53
+ /** Speech-to-text */
54
+ speechToText?(audio: ArrayBuffer | Blob, options?: { language?: string }): Promise<STTResult>;
55
+
56
+ /** List available voices */
57
+ listVoices?(): Promise<Array<{ id: string; name: string; preview_url?: string }>>;
58
+ }
59
+
60
+ /** OpenAI voice IDs */
61
+ export type OpenAIVoice = 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
62
+
63
+ /**
64
+ * OpenAI Voice Provider
65
+ *
66
+ * @example
67
+ * const voice = new OpenAIVoiceProvider();
68
+ * const audio = await voice.textToSpeech('Hello, world!', { voiceId: 'nova' });
69
+ */
70
+ export class OpenAIVoiceProvider implements VoiceProvider {
71
+ private readonly apiKey: string;
72
+ private readonly baseUrl: string;
73
+
74
+ constructor(config?: { apiKey?: string; baseUrl?: string }) {
75
+ this.apiKey = config?.apiKey ?? process.env.OPENAI_API_KEY ?? '';
76
+ this.baseUrl = config?.baseUrl ?? 'https://api.openai.com/v1';
77
+
78
+ if (!this.apiKey) {
79
+ throw new Error('OpenAI API key required for voice provider');
80
+ }
81
+ }
82
+
83
+ async textToSpeech(text: string, options?: Partial<VoiceConfig>): Promise<TTSResult> {
84
+ const voice = (options?.voiceId ?? 'alloy') as OpenAIVoice;
85
+ const model = options?.model ?? 'tts-1';
86
+ const speed = options?.speed ?? 1.0;
87
+
88
+ const response = await fetch(`${this.baseUrl}/audio/speech`, {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Authorization': `Bearer ${this.apiKey}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify({
95
+ model,
96
+ input: text,
97
+ voice,
98
+ speed,
99
+ response_format: 'mp3',
100
+ }),
101
+ });
102
+
103
+ if (!response.ok) {
104
+ throw new Error(`OpenAI TTS failed: ${response.status} ${await response.text()}`);
105
+ }
106
+
107
+ const audio = await response.arrayBuffer();
108
+
109
+ return {
110
+ audio,
111
+ format: 'mp3',
112
+ characterCount: text.length,
113
+ };
114
+ }
115
+
116
+ async speechToText(audio: ArrayBuffer | Blob, options?: { language?: string }): Promise<STTResult> {
117
+ const formData = new FormData();
118
+
119
+ const blob = audio instanceof Blob ? audio : new Blob([audio], { type: 'audio/wav' });
120
+ formData.append('file', blob, 'audio.wav');
121
+ formData.append('model', 'whisper-1');
122
+
123
+ if (options?.language) {
124
+ formData.append('language', options.language);
125
+ }
126
+
127
+ const response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Authorization': `Bearer ${this.apiKey}`,
131
+ },
132
+ body: formData,
133
+ });
134
+
135
+ if (!response.ok) {
136
+ throw new Error(`OpenAI STT failed: ${response.status} ${await response.text()}`);
137
+ }
138
+
139
+ const result = await response.json() as { text: string; language?: string };
140
+
141
+ return {
142
+ text: result.text,
143
+ language: result.language,
144
+ };
145
+ }
146
+
147
+ async listVoices(): Promise<Array<{ id: string; name: string }>> {
148
+ return [
149
+ { id: 'alloy', name: 'Alloy' },
150
+ { id: 'echo', name: 'Echo' },
151
+ { id: 'fable', name: 'Fable' },
152
+ { id: 'onyx', name: 'Onyx' },
153
+ { id: 'nova', name: 'Nova' },
154
+ { id: 'shimmer', name: 'Shimmer' },
155
+ ];
156
+ }
157
+ }
158
+
159
+ /**
160
+ * ElevenLabs Voice Provider (stub - requires implementation)
161
+ */
162
+ export class ElevenLabsVoiceProvider implements VoiceProvider {
163
+ private readonly apiKey: string;
164
+ private readonly baseUrl = 'https://api.elevenlabs.io/v1';
165
+
166
+ constructor(config?: { apiKey?: string }) {
167
+ this.apiKey = config?.apiKey ?? process.env.ELEVENLABS_API_KEY ?? '';
168
+ }
169
+
170
+ async textToSpeech(text: string, options?: Partial<VoiceConfig>): Promise<TTSResult> {
171
+ const voiceId = options?.voiceId ?? '21m00Tcm4TlvDq8ikWAM'; // Rachel
172
+
173
+ const response = await fetch(`${this.baseUrl}/text-to-speech/${voiceId}`, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'xi-api-key': this.apiKey,
177
+ 'Content-Type': 'application/json',
178
+ },
179
+ body: JSON.stringify({
180
+ text,
181
+ model_id: options?.model ?? 'eleven_monolingual_v1',
182
+ }),
183
+ });
184
+
185
+ if (!response.ok) {
186
+ throw new Error(`ElevenLabs TTS failed: ${response.status}`);
187
+ }
188
+
189
+ const audio = await response.arrayBuffer();
190
+
191
+ return {
192
+ audio,
193
+ format: 'mp3',
194
+ characterCount: text.length,
195
+ };
196
+ }
197
+
198
+ async listVoices(): Promise<Array<{ id: string; name: string; preview_url?: string }>> {
199
+ const response = await fetch(`${this.baseUrl}/voices`, {
200
+ headers: { 'xi-api-key': this.apiKey },
201
+ });
202
+
203
+ if (!response.ok) {
204
+ throw new Error(`ElevenLabs list voices failed: ${response.status}`);
205
+ }
206
+
207
+ const data = await response.json() as { voices: Array<{ voice_id: string; name: string; preview_url?: string }> };
208
+ return data.voices.map((v) => ({
209
+ id: v.voice_id,
210
+ name: v.name,
211
+ preview_url: v.preview_url,
212
+ }));
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Create a voice provider based on configuration
218
+ */
219
+ export function createVoiceProvider(config: VoiceConfig): VoiceProvider {
220
+ switch (config.provider) {
221
+ case 'openai':
222
+ return new OpenAIVoiceProvider({ apiKey: config.apiKey });
223
+ case 'elevenlabs':
224
+ return new ElevenLabsVoiceProvider({ apiKey: config.apiKey });
225
+ default:
226
+ throw new Error(`Unknown voice provider: ${config.provider}`);
227
+ }
228
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Artifact System Unit Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import {
7
+ InMemoryArtifactStorage,
8
+ createTextArtifact,
9
+ createMarkdownArtifact,
10
+ createDataArtifact,
11
+ createReasoningArtifact,
12
+ createPlanArtifact,
13
+ } from '../src/artifacts/artifact.js';
14
+ import type { ArtifactStorage } from '../src/artifacts/artifact.js';
15
+
16
+ describe('InMemoryArtifactStorage', () => {
17
+ let storage: ArtifactStorage;
18
+
19
+ beforeEach(() => {
20
+ storage = new InMemoryArtifactStorage();
21
+ });
22
+
23
+ describe('save', () => {
24
+ it('should save an artifact and return with generated ID', async () => {
25
+ const artifact = createTextArtifact('test.txt', 'Hello, World!');
26
+ const saved = await storage.save(artifact);
27
+
28
+ expect(saved.id).toBeDefined();
29
+ expect(saved.id).toMatch(/^art_/);
30
+ expect(saved.name).toBe('test.txt');
31
+ expect(saved.content).toBe('Hello, World!');
32
+ expect(saved.version).toBe(1);
33
+ expect(saved.createdAt).toBeInstanceOf(Date);
34
+ });
35
+ });
36
+
37
+ describe('get', () => {
38
+ it('should retrieve a saved artifact', async () => {
39
+ const artifact = createTextArtifact('test.txt', 'content');
40
+ const saved = await storage.save(artifact);
41
+
42
+ const retrieved = await storage.get(saved.id);
43
+
44
+ expect(retrieved).not.toBeNull();
45
+ expect(retrieved?.id).toBe(saved.id);
46
+ expect(retrieved?.content).toBe('content');
47
+ });
48
+
49
+ it('should return null for non-existent artifact', async () => {
50
+ const retrieved = await storage.get('non-existent');
51
+ expect(retrieved).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe('update', () => {
56
+ it('should update an artifact with new version', async () => {
57
+ const artifact = createTextArtifact('test.txt', 'v1');
58
+ const saved = await storage.save(artifact);
59
+
60
+ const updated = await storage.update(saved.id, { content: 'v2' });
61
+
62
+ expect(updated.version).toBe(2);
63
+ expect(updated.content).toBe('v2');
64
+ });
65
+
66
+ it('should throw for non-existent artifact', async () => {
67
+ await expect(
68
+ storage.update('non-existent', { content: 'test' })
69
+ ).rejects.toThrow('Artifact not found');
70
+ });
71
+ });
72
+
73
+ describe('versioning', () => {
74
+ it('should list all versions of an artifact', async () => {
75
+ const artifact = createTextArtifact('test.txt', 'v1');
76
+ const saved = await storage.save(artifact);
77
+ await storage.update(saved.id, { content: 'v2' });
78
+ await storage.update(saved.id, { content: 'v3' });
79
+
80
+ const versions = await storage.listVersions(saved.id);
81
+
82
+ expect(versions).toHaveLength(3);
83
+ expect(versions[0].version).toBe(1);
84
+ expect(versions[2].version).toBe(3);
85
+ });
86
+
87
+ it('should get a specific version', async () => {
88
+ const artifact = createTextArtifact('test.txt', 'v1');
89
+ const saved = await storage.save(artifact);
90
+ await storage.update(saved.id, { content: 'v2' });
91
+
92
+ const v1 = await storage.getVersion(saved.id, 1);
93
+ const v2 = await storage.getVersion(saved.id, 2);
94
+
95
+ expect(v1?.content).toBe('v1');
96
+ expect(v2?.content).toBe('v2');
97
+ });
98
+ });
99
+
100
+ describe('delete', () => {
101
+ it('should delete an artifact', async () => {
102
+ const artifact = createTextArtifact('test.txt', 'content');
103
+ const saved = await storage.save(artifact);
104
+
105
+ const deleted = await storage.delete(saved.id);
106
+ const retrieved = await storage.get(saved.id);
107
+
108
+ expect(deleted).toBe(true);
109
+ expect(retrieved).toBeNull();
110
+ });
111
+
112
+ it('should return false for non-existent artifact', async () => {
113
+ const deleted = await storage.delete('non-existent');
114
+ expect(deleted).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('list', () => {
119
+ it('should list all artifacts', async () => {
120
+ await storage.save(createTextArtifact('a.txt', 'a'));
121
+ await storage.save(createTextArtifact('b.txt', 'b'));
122
+
123
+ const list = await storage.list();
124
+
125
+ expect(list).toHaveLength(2);
126
+ });
127
+
128
+ it('should filter by type', async () => {
129
+ await storage.save(createTextArtifact('file.txt', 'text'));
130
+ await storage.save(createMarkdownArtifact('doc.md', '# Heading'));
131
+
132
+ const markdownOnly = await storage.list({ type: 'markdown' });
133
+
134
+ expect(markdownOnly).toHaveLength(1);
135
+ expect(markdownOnly[0].type).toBe('markdown');
136
+ });
137
+
138
+ it('should filter by tags', async () => {
139
+ await storage.save(createTextArtifact('a.txt', 'a', { tags: ['project-a'] }));
140
+ await storage.save(createTextArtifact('b.txt', 'b', { tags: ['project-b'] }));
141
+
142
+ const projectA = await storage.list({ tags: ['project-a'] });
143
+
144
+ expect(projectA).toHaveLength(1);
145
+ expect(projectA[0].name).toBe('a.txt');
146
+ });
147
+
148
+ it('should support pagination', async () => {
149
+ await storage.save(createTextArtifact('1.txt', '1'));
150
+ await storage.save(createTextArtifact('2.txt', '2'));
151
+ await storage.save(createTextArtifact('3.txt', '3'));
152
+
153
+ const page1 = await storage.list({ limit: 2, offset: 0 });
154
+ const page2 = await storage.list({ limit: 2, offset: 2 });
155
+
156
+ expect(page1).toHaveLength(2);
157
+ expect(page2).toHaveLength(1);
158
+ });
159
+ });
160
+
161
+ describe('search', () => {
162
+ it('should search artifacts by name', async () => {
163
+ await storage.save(createTextArtifact('report-2024.txt', 'content'));
164
+ await storage.save(createTextArtifact('notes.txt', 'content'));
165
+
166
+ const results = await storage.search('report');
167
+
168
+ expect(results).toHaveLength(1);
169
+ expect(results[0].name).toBe('report-2024.txt');
170
+ });
171
+ });
172
+ });
173
+
174
+ describe('Artifact Helpers', () => {
175
+ describe('createTextArtifact', () => {
176
+ it('should create a text artifact', () => {
177
+ const artifact = createTextArtifact('file.txt', 'content');
178
+
179
+ expect(artifact.name).toBe('file.txt');
180
+ expect(artifact.type).toBe('file');
181
+ expect(artifact.content).toBe('content');
182
+ expect(artifact.mimeType).toBe('text/plain');
183
+ });
184
+ });
185
+
186
+ describe('createMarkdownArtifact', () => {
187
+ it('should create a markdown artifact', () => {
188
+ const artifact = createMarkdownArtifact('doc.md', '# Title');
189
+
190
+ expect(artifact.type).toBe('markdown');
191
+ expect(artifact.mimeType).toBe('text/markdown');
192
+ });
193
+ });
194
+
195
+ describe('createDataArtifact', () => {
196
+ it('should create a JSON data artifact', () => {
197
+ const data = { key: 'value', count: 42 };
198
+ const artifact = createDataArtifact('data.json', data);
199
+
200
+ expect(artifact.type).toBe('data');
201
+ expect(artifact.content).toEqual(data);
202
+ expect(artifact.mimeType).toBe('application/json');
203
+ });
204
+ });
205
+
206
+ describe('createReasoningArtifact', () => {
207
+ it('should create a reasoning artifact', () => {
208
+ const artifact = createReasoningArtifact(
209
+ 'analysis',
210
+ ['First thought', 'Second thought'],
211
+ 'Final conclusion',
212
+ 0.85
213
+ );
214
+
215
+ expect(artifact.type).toBe('reasoning');
216
+ expect(artifact.content.thoughts).toHaveLength(2);
217
+ expect(artifact.content.conclusion).toBe('Final conclusion');
218
+ expect(artifact.content.confidence).toBe(0.85);
219
+ });
220
+ });
221
+
222
+ describe('createPlanArtifact', () => {
223
+ it('should create a plan artifact with steps', () => {
224
+ const artifact = createPlanArtifact(
225
+ 'project-plan',
226
+ 'Build a feature',
227
+ [
228
+ { description: 'Research requirements' },
229
+ { description: 'Implement solution' },
230
+ { description: 'Test and deploy' },
231
+ ]
232
+ );
233
+
234
+ expect(artifact.type).toBe('plan');
235
+ expect(artifact.content.goal).toBe('Build a feature');
236
+ expect(artifact.content.steps).toHaveLength(3);
237
+ expect(artifact.content.steps[0].status).toBe('pending');
238
+ expect(artifact.content.status).toBe('draft');
239
+ });
240
+ });
241
+ });