cognitive-modules-cli 2.2.1 → 2.2.7

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 (101) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +519 -23
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +383 -16
  7. package/dist/commands/compose.js +60 -23
  8. package/dist/commands/index.d.ts +4 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/init.js +23 -1
  11. package/dist/commands/migrate.d.ts +30 -0
  12. package/dist/commands/migrate.js +650 -0
  13. package/dist/commands/pipe.d.ts +1 -0
  14. package/dist/commands/pipe.js +31 -11
  15. package/dist/commands/remove.js +33 -2
  16. package/dist/commands/run.d.ts +2 -0
  17. package/dist/commands/run.js +61 -28
  18. package/dist/commands/search.d.ts +28 -0
  19. package/dist/commands/search.js +143 -0
  20. package/dist/commands/test.d.ts +65 -0
  21. package/dist/commands/test.js +454 -0
  22. package/dist/commands/update.d.ts +1 -0
  23. package/dist/commands/update.js +106 -14
  24. package/dist/commands/validate.d.ts +36 -0
  25. package/dist/commands/validate.js +97 -0
  26. package/dist/errors/index.d.ts +225 -0
  27. package/dist/errors/index.js +420 -0
  28. package/dist/mcp/server.js +84 -79
  29. package/dist/modules/composition.js +97 -32
  30. package/dist/modules/loader.js +4 -2
  31. package/dist/modules/runner.d.ts +72 -5
  32. package/dist/modules/runner.js +306 -59
  33. package/dist/modules/subagent.d.ts +6 -1
  34. package/dist/modules/subagent.js +18 -13
  35. package/dist/modules/validator.js +14 -6
  36. package/dist/providers/anthropic.d.ts +15 -0
  37. package/dist/providers/anthropic.js +147 -5
  38. package/dist/providers/base.d.ts +11 -0
  39. package/dist/providers/base.js +18 -0
  40. package/dist/providers/gemini.d.ts +15 -0
  41. package/dist/providers/gemini.js +122 -5
  42. package/dist/providers/ollama.d.ts +15 -0
  43. package/dist/providers/ollama.js +111 -3
  44. package/dist/providers/openai.d.ts +11 -0
  45. package/dist/providers/openai.js +133 -0
  46. package/dist/registry/client.d.ts +212 -0
  47. package/dist/registry/client.js +359 -0
  48. package/dist/registry/index.d.ts +4 -0
  49. package/dist/registry/index.js +4 -0
  50. package/dist/registry/tar.d.ts +8 -0
  51. package/dist/registry/tar.js +353 -0
  52. package/dist/server/http.js +301 -45
  53. package/dist/server/index.d.ts +2 -0
  54. package/dist/server/index.js +1 -0
  55. package/dist/server/sse.d.ts +13 -0
  56. package/dist/server/sse.js +22 -0
  57. package/dist/types.d.ts +32 -1
  58. package/dist/types.js +4 -1
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +4 -0
  61. package/package.json +31 -7
  62. package/dist/modules/composition.test.d.ts +0 -11
  63. package/dist/modules/composition.test.js +0 -450
  64. package/dist/modules/policy.test.d.ts +0 -10
  65. package/dist/modules/policy.test.js +0 -369
  66. package/src/cli.ts +0 -471
  67. package/src/commands/add.ts +0 -315
  68. package/src/commands/compose.ts +0 -185
  69. package/src/commands/index.ts +0 -13
  70. package/src/commands/init.ts +0 -94
  71. package/src/commands/list.ts +0 -33
  72. package/src/commands/pipe.ts +0 -76
  73. package/src/commands/remove.ts +0 -57
  74. package/src/commands/run.ts +0 -80
  75. package/src/commands/update.ts +0 -130
  76. package/src/commands/versions.ts +0 -79
  77. package/src/index.ts +0 -90
  78. package/src/mcp/index.ts +0 -5
  79. package/src/mcp/server.ts +0 -403
  80. package/src/modules/composition.test.ts +0 -558
  81. package/src/modules/composition.ts +0 -1674
  82. package/src/modules/index.ts +0 -9
  83. package/src/modules/loader.ts +0 -508
  84. package/src/modules/policy.test.ts +0 -455
  85. package/src/modules/runner.ts +0 -1983
  86. package/src/modules/subagent.ts +0 -277
  87. package/src/modules/validator.ts +0 -700
  88. package/src/providers/anthropic.ts +0 -89
  89. package/src/providers/base.ts +0 -29
  90. package/src/providers/deepseek.ts +0 -83
  91. package/src/providers/gemini.ts +0 -117
  92. package/src/providers/index.ts +0 -78
  93. package/src/providers/minimax.ts +0 -81
  94. package/src/providers/moonshot.ts +0 -82
  95. package/src/providers/ollama.ts +0 -83
  96. package/src/providers/openai.ts +0 -84
  97. package/src/providers/qwen.ts +0 -82
  98. package/src/server/http.ts +0 -316
  99. package/src/server/index.ts +0 -6
  100. package/src/types.ts +0 -599
  101. package/tsconfig.json +0 -17
@@ -75,11 +75,15 @@ export function parseCalls(text) {
75
75
  */
76
76
  export function substituteCallResults(text, callResults) {
77
77
  let result = text;
78
- for (const [callStr, callResult] of Object.entries(callResults)) {
79
- const resultStr = typeof callResult === 'object'
80
- ? JSON.stringify(callResult, null, 2)
81
- : String(callResult);
82
- result = result.replace(callStr, `[Result from ${callStr}]:\n${resultStr}`);
78
+ for (const entry of callResults) {
79
+ const resultStr = typeof entry.result === 'object'
80
+ ? JSON.stringify(entry.result, null, 2)
81
+ : String(entry.result);
82
+ const replacement = `[Result from ${entry.module}]:\n${resultStr}`;
83
+ const idx = result.indexOf(entry.match);
84
+ if (idx !== -1) {
85
+ result = result.slice(0, idx) + replacement + result.slice(idx + entry.match.length);
86
+ }
83
87
  }
84
88
  return result;
85
89
  }
@@ -99,7 +103,7 @@ export class SubagentOrchestrator {
99
103
  * Recursively resolves @call directives before final execution.
100
104
  */
101
105
  async run(moduleName, options = {}, context) {
102
- const { input = {}, validateInput = true, validateOutput = true, maxDepth = 5 } = options;
106
+ const { input = {}, args, validateInput = true, validateOutput = true, maxDepth = 5 } = options;
103
107
  // Initialize context
104
108
  const ctx = context ?? createContext(maxDepth);
105
109
  // Check depth limit
@@ -122,15 +126,14 @@ export class SubagentOrchestrator {
122
126
  const moduleContextMode = module.context ?? 'main';
123
127
  // Parse @call directives from prompt
124
128
  const calls = parseCalls(module.prompt);
125
- const callResults = {};
129
+ const callResults = [];
126
130
  // Resolve each @call directive
127
131
  for (const call of calls) {
128
132
  const childModule = call.module;
129
133
  const childArgs = call.args;
130
134
  // Prepare child input
131
- const childInput = childArgs
132
- ? { query: childArgs, code: childArgs }
133
- : { ...input };
135
+ const hasChildArgs = childArgs.length > 0;
136
+ const childInput = hasChildArgs ? {} : { ...input };
134
137
  // Determine child context
135
138
  const childContext = moduleContextMode === 'fork'
136
139
  ? forkContext(ctx, moduleName)
@@ -138,20 +141,21 @@ export class SubagentOrchestrator {
138
141
  // Recursively run child module
139
142
  const childResult = await this.run(childModule, {
140
143
  input: childInput,
144
+ args: hasChildArgs ? childArgs : undefined,
141
145
  validateInput: false, // Skip validation for @call args
142
146
  validateOutput
143
147
  }, childContext);
144
148
  // Store result
145
149
  if (childResult.ok && 'data' in childResult) {
146
- callResults[call.match] = childResult.data;
150
+ callResults.push({ match: call.match, module: call.module, result: childResult.data });
147
151
  }
148
152
  else if ('error' in childResult) {
149
- callResults[call.match] = { error: childResult.error };
153
+ callResults.push({ match: call.match, module: call.module, result: { error: childResult.error } });
150
154
  }
151
155
  }
152
156
  // Substitute call results into prompt
153
157
  let modifiedModule = module;
154
- if (Object.keys(callResults).length > 0) {
158
+ if (callResults.length > 0) {
155
159
  const modifiedPrompt = substituteCallResults(module.prompt, callResults);
156
160
  modifiedModule = {
157
161
  ...module,
@@ -161,6 +165,7 @@ export class SubagentOrchestrator {
161
165
  // Run the module
162
166
  const result = await runModule(modifiedModule, this.provider, {
163
167
  input,
168
+ args,
164
169
  validateInput,
165
170
  validateOutput,
166
171
  verbose: false,
@@ -45,7 +45,7 @@ export async function validateModule(modulePath, v22 = false) {
45
45
  else if (hasModuleMd) {
46
46
  // v1 format
47
47
  if (v22) {
48
- errors.push("Module is v1 format. Use 'cogn migrate' to upgrade to v2.2");
48
+ errors.push("Module is v1 format. Use 'cog migrate' to upgrade to v2.2");
49
49
  return { valid: false, errors, warnings };
50
50
  }
51
51
  return validateV1Format(modulePath);
@@ -53,7 +53,7 @@ export async function validateModule(modulePath, v22 = false) {
53
53
  else if (hasOldModuleMd) {
54
54
  // v0 format
55
55
  if (v22) {
56
- errors.push("Module is v0 format. Use 'cogn migrate' to upgrade to v2.2");
56
+ errors.push("Module is v0 format. Use 'cog migrate' to upgrade to v2.2");
57
57
  return { valid: false, errors, warnings };
58
58
  }
59
59
  return validateV0Format(modulePath);
@@ -73,7 +73,8 @@ async function validateV22Format(modulePath) {
73
73
  let manifest;
74
74
  try {
75
75
  const content = await fs.readFile(moduleYamlPath, 'utf-8');
76
- manifest = yaml.load(content);
76
+ const loaded = yaml.load(content);
77
+ manifest = loaded && typeof loaded === 'object' ? loaded : {};
77
78
  }
78
79
  catch (e) {
79
80
  errors.push(`Invalid YAML in module.yaml: ${e.message}`);
@@ -244,7 +245,8 @@ async function validateV2Format(modulePath) {
244
245
  let manifest;
245
246
  try {
246
247
  const content = await fs.readFile(moduleYamlPath, 'utf-8');
247
- manifest = yaml.load(content);
248
+ const loaded = yaml.load(content);
249
+ manifest = loaded && typeof loaded === 'object' ? loaded : {};
248
250
  }
249
251
  catch (e) {
250
252
  errors.push(`Invalid YAML in module.yaml: ${e.message}`);
@@ -297,7 +299,7 @@ async function validateV2Format(modulePath) {
297
299
  }
298
300
  // Check for v2.2 features and suggest upgrade
299
301
  if (!manifest.tier) {
300
- warnings.push("Consider adding 'tier' for v2.2 (use 'cogn validate --v22' for full check)");
302
+ warnings.push("Consider adding 'tier' for v2.2 (use 'cog validate --v22' for full check)");
301
303
  }
302
304
  return { valid: errors.length === 0, errors, warnings };
303
305
  }
@@ -573,6 +575,9 @@ export function validateV22Envelope(response) {
573
575
  if (!('meta' in response)) {
574
576
  errors.push("Missing 'meta' field (required for v2.2)");
575
577
  }
578
+ else if (typeof response.meta !== 'object' || response.meta === null || Array.isArray(response.meta)) {
579
+ errors.push("meta must be an object");
580
+ }
576
581
  else {
577
582
  const meta = response.meta;
578
583
  if (!('confidence' in meta)) {
@@ -596,8 +601,11 @@ export function validateV22Envelope(response) {
596
601
  if (!('explain' in meta)) {
597
602
  errors.push("meta missing 'explain'");
598
603
  }
604
+ else if (typeof meta.explain !== 'string') {
605
+ errors.push("meta.explain must be a string");
606
+ }
599
607
  else {
600
- const explain = meta.explain ?? '';
608
+ const explain = meta.explain;
601
609
  if (explain.length > 280) {
602
610
  errors.push(`meta.explain exceeds 280 chars (${explain.length} chars)`);
603
611
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Anthropic Provider - Claude API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  import type { InvokeParams, InvokeResult } from '../types.js';
@@ -10,5 +12,18 @@ export declare class AnthropicProvider extends BaseProvider {
10
12
  private baseUrl;
11
13
  constructor(apiKey?: string, model?: string);
12
14
  isConfigured(): boolean;
15
+ /**
16
+ * Anthropic supports streaming.
17
+ */
18
+ supportsStreaming(): boolean;
19
+ /**
20
+ * Build request body for Anthropic API
21
+ */
22
+ private buildRequestBody;
13
23
  invoke(params: InvokeParams): Promise<InvokeResult>;
24
+ /**
25
+ * Stream-based invoke using Anthropic's streaming API.
26
+ * Yields content chunks as they arrive from the API.
27
+ */
28
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
14
29
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Anthropic Provider - Claude API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  export class AnthropicProvider extends BaseProvider {
@@ -15,11 +17,16 @@ export class AnthropicProvider extends BaseProvider {
15
17
  isConfigured() {
16
18
  return !!this.apiKey;
17
19
  }
18
- async invoke(params) {
19
- if (!this.isConfigured()) {
20
- throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
21
- }
22
- const url = `${this.baseUrl}/messages`;
20
+ /**
21
+ * Anthropic supports streaming.
22
+ */
23
+ supportsStreaming() {
24
+ return true;
25
+ }
26
+ /**
27
+ * Build request body for Anthropic API
28
+ */
29
+ buildRequestBody(params, stream) {
23
30
  // Extract system message
24
31
  const systemMessage = params.messages.find(m => m.role === 'system');
25
32
  const otherMessages = params.messages.filter(m => m.role !== 'system');
@@ -39,10 +46,19 @@ export class AnthropicProvider extends BaseProvider {
39
46
  model: this.model,
40
47
  messages: messages.map(m => ({ role: m.role, content: m.content })),
41
48
  max_tokens: params.maxTokens ?? 4096,
49
+ stream,
42
50
  };
43
51
  if (systemMessage) {
44
52
  body.system = systemMessage.content;
45
53
  }
54
+ return { body, systemContent: systemMessage?.content };
55
+ }
56
+ async invoke(params) {
57
+ if (!this.isConfigured()) {
58
+ throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
59
+ }
60
+ const url = `${this.baseUrl}/messages`;
61
+ const { body } = this.buildRequestBody(params, false);
46
62
  const response = await fetch(url, {
47
63
  method: 'POST',
48
64
  headers: {
@@ -67,4 +83,130 @@ export class AnthropicProvider extends BaseProvider {
67
83
  } : undefined,
68
84
  };
69
85
  }
86
+ /**
87
+ * Stream-based invoke using Anthropic's streaming API.
88
+ * Yields content chunks as they arrive from the API.
89
+ */
90
+ async *invokeStream(params) {
91
+ if (!this.isConfigured()) {
92
+ throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
93
+ }
94
+ const url = `${this.baseUrl}/messages`;
95
+ const { body } = this.buildRequestBody(params, true);
96
+ const response = await fetch(url, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ 'x-api-key': this.apiKey,
101
+ 'anthropic-version': '2023-06-01',
102
+ },
103
+ body: JSON.stringify(body),
104
+ });
105
+ if (!response.ok) {
106
+ const error = await response.text();
107
+ throw new Error(`Anthropic API error: ${response.status} - ${error}`);
108
+ }
109
+ if (!response.body) {
110
+ throw new Error('Anthropic API returned no body for streaming request');
111
+ }
112
+ const reader = response.body.getReader();
113
+ const decoder = new TextDecoder('utf-8');
114
+ const collectedChunks = [];
115
+ let usage;
116
+ let buffer = '';
117
+ try {
118
+ while (true) {
119
+ const { done, value } = await reader.read();
120
+ if (done)
121
+ break;
122
+ buffer += decoder.decode(value, { stream: true });
123
+ // Process complete lines from the buffer
124
+ const lines = buffer.split('\n');
125
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ if (!trimmed || trimmed.startsWith(':'))
129
+ continue; // Skip empty lines and comments
130
+ if (trimmed.startsWith('data: ')) {
131
+ try {
132
+ const data = JSON.parse(trimmed.slice(6));
133
+ // Extract content chunk (content_block_delta event)
134
+ if (data.type === 'content_block_delta' && data.delta?.type === 'text_delta') {
135
+ const text = data.delta.text || '';
136
+ if (text) {
137
+ collectedChunks.push(text);
138
+ yield text;
139
+ }
140
+ }
141
+ // Extract usage info (message_delta or message_stop event)
142
+ if (data.type === 'message_delta' && data.usage) {
143
+ usage = {
144
+ promptTokens: data.usage.input_tokens || 0,
145
+ completionTokens: data.usage.output_tokens || 0,
146
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
147
+ };
148
+ }
149
+ // Also check message_start for input tokens
150
+ if (data.type === 'message_start' && data.message?.usage) {
151
+ const inputTokens = data.message.usage.input_tokens || 0;
152
+ usage = {
153
+ promptTokens: inputTokens,
154
+ completionTokens: usage?.completionTokens || 0,
155
+ totalTokens: inputTokens + (usage?.completionTokens || 0),
156
+ };
157
+ }
158
+ }
159
+ catch {
160
+ // Skip invalid JSON chunks
161
+ }
162
+ }
163
+ }
164
+ }
165
+ // Flush decoder and process trailing buffered data even without trailing newline.
166
+ buffer += decoder.decode();
167
+ for (const line of buffer.split('\n')) {
168
+ const trimmed = line.trim();
169
+ if (!trimmed || trimmed.startsWith(':'))
170
+ continue;
171
+ if (trimmed.startsWith('data: ')) {
172
+ try {
173
+ const data = JSON.parse(trimmed.slice(6));
174
+ if (data.type === 'content_block_delta' && data.delta?.type === 'text_delta') {
175
+ const text = data.delta.text || '';
176
+ if (text) {
177
+ collectedChunks.push(text);
178
+ yield text;
179
+ }
180
+ }
181
+ if (data.type === 'message_delta' && data.usage) {
182
+ usage = {
183
+ promptTokens: data.usage.input_tokens || 0,
184
+ completionTokens: data.usage.output_tokens || 0,
185
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
186
+ };
187
+ }
188
+ if (data.type === 'message_start' && data.message?.usage) {
189
+ const inputTokens = data.message.usage.input_tokens || 0;
190
+ usage = {
191
+ promptTokens: inputTokens,
192
+ completionTokens: usage?.completionTokens || 0,
193
+ totalTokens: inputTokens + (usage?.completionTokens || 0),
194
+ };
195
+ }
196
+ }
197
+ catch {
198
+ // Skip invalid JSON chunks
199
+ }
200
+ }
201
+ }
202
+ }
203
+ finally {
204
+ reader.releaseLock();
205
+ }
206
+ const fullContent = collectedChunks.join('');
207
+ return {
208
+ content: fullContent,
209
+ usage,
210
+ };
211
+ }
70
212
  }
@@ -6,6 +6,17 @@ export declare abstract class BaseProvider implements Provider {
6
6
  abstract name: string;
7
7
  abstract invoke(params: InvokeParams): Promise<InvokeResult>;
8
8
  abstract isConfigured(): boolean;
9
+ /**
10
+ * Check if this provider supports streaming.
11
+ * Override in subclasses that implement streaming.
12
+ */
13
+ supportsStreaming(): boolean;
14
+ /**
15
+ * Stream-based invoke (optional).
16
+ * Default implementation falls back to non-streaming invoke.
17
+ * Override in subclasses that support streaming.
18
+ */
19
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
9
20
  protected buildJsonPrompt(schema: object): string;
10
21
  protected parseJsonResponse(content: string): unknown;
11
22
  }
@@ -2,6 +2,24 @@
2
2
  * Base Provider - Abstract class for all LLM providers
3
3
  */
4
4
  export class BaseProvider {
5
+ /**
6
+ * Check if this provider supports streaming.
7
+ * Override in subclasses that implement streaming.
8
+ */
9
+ supportsStreaming() {
10
+ return false;
11
+ }
12
+ /**
13
+ * Stream-based invoke (optional).
14
+ * Default implementation falls back to non-streaming invoke.
15
+ * Override in subclasses that support streaming.
16
+ */
17
+ async *invokeStream(params) {
18
+ // Default fallback: use non-streaming invoke and yield the entire result at once
19
+ const result = await this.invoke(params);
20
+ yield result.content;
21
+ return result;
22
+ }
5
23
  buildJsonPrompt(schema) {
6
24
  return `\n\nYou MUST respond with valid JSON matching this schema:\n${JSON.stringify(schema, null, 2)}\n\nRespond with ONLY the JSON, no markdown code blocks.`;
7
25
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Gemini Provider - Google Gemini API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  import type { InvokeParams, InvokeResult } from '../types.js';
@@ -10,10 +12,23 @@ export declare class GeminiProvider extends BaseProvider {
10
12
  private baseUrl;
11
13
  constructor(apiKey?: string, model?: string);
12
14
  isConfigured(): boolean;
15
+ /**
16
+ * Gemini supports streaming.
17
+ */
18
+ supportsStreaming(): boolean;
13
19
  /**
14
20
  * Clean JSON Schema for Gemini API compatibility
15
21
  * Removes unsupported fields like additionalProperties
16
22
  */
17
23
  private cleanSchemaForGemini;
24
+ /**
25
+ * Build request body for Gemini API
26
+ */
27
+ private buildRequestBody;
18
28
  invoke(params: InvokeParams): Promise<InvokeResult>;
29
+ /**
30
+ * Stream-based invoke using Gemini's streaming API.
31
+ * Yields content chunks as they arrive from the API.
32
+ */
33
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
19
34
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Gemini Provider - Google Gemini API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  export class GeminiProvider extends BaseProvider {
@@ -15,6 +17,12 @@ export class GeminiProvider extends BaseProvider {
15
17
  isConfigured() {
16
18
  return !!this.apiKey;
17
19
  }
20
+ /**
21
+ * Gemini supports streaming.
22
+ */
23
+ supportsStreaming() {
24
+ return true;
25
+ }
18
26
  /**
19
27
  * Clean JSON Schema for Gemini API compatibility
20
28
  * Removes unsupported fields like additionalProperties
@@ -38,11 +46,10 @@ export class GeminiProvider extends BaseProvider {
38
46
  };
39
47
  return clean(schema);
40
48
  }
41
- async invoke(params) {
42
- if (!this.isConfigured()) {
43
- throw new Error('Gemini API key not configured. Set GEMINI_API_KEY environment variable.');
44
- }
45
- const url = `${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`;
49
+ /**
50
+ * Build request body for Gemini API
51
+ */
52
+ buildRequestBody(params) {
46
53
  // Convert messages to Gemini format
47
54
  const contents = params.messages
48
55
  .filter(m => m.role !== 'system')
@@ -71,6 +78,14 @@ export class GeminiProvider extends BaseProvider {
71
78
  responseSchema: cleanedSchema,
72
79
  };
73
80
  }
81
+ return body;
82
+ }
83
+ async invoke(params) {
84
+ if (!this.isConfigured()) {
85
+ throw new Error('Gemini API key not configured. Set GEMINI_API_KEY environment variable.');
86
+ }
87
+ const url = `${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`;
88
+ const body = this.buildRequestBody(params);
74
89
  const response = await fetch(url, {
75
90
  method: 'POST',
76
91
  headers: { 'Content-Type': 'application/json' },
@@ -91,4 +106,106 @@ export class GeminiProvider extends BaseProvider {
91
106
  } : undefined,
92
107
  };
93
108
  }
109
+ /**
110
+ * Stream-based invoke using Gemini's streaming API.
111
+ * Yields content chunks as they arrive from the API.
112
+ */
113
+ async *invokeStream(params) {
114
+ if (!this.isConfigured()) {
115
+ throw new Error('Gemini API key not configured. Set GEMINI_API_KEY environment variable.');
116
+ }
117
+ // Use streamGenerateContent endpoint
118
+ const url = `${this.baseUrl}/models/${this.model}:streamGenerateContent?key=${this.apiKey}&alt=sse`;
119
+ const body = this.buildRequestBody(params);
120
+ const response = await fetch(url, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify(body),
124
+ });
125
+ if (!response.ok) {
126
+ const error = await response.text();
127
+ throw new Error(`Gemini API error: ${response.status} - ${error}`);
128
+ }
129
+ if (!response.body) {
130
+ throw new Error('Gemini API returned no body for streaming request');
131
+ }
132
+ const reader = response.body.getReader();
133
+ const decoder = new TextDecoder('utf-8');
134
+ const collectedChunks = [];
135
+ let usage;
136
+ let buffer = '';
137
+ try {
138
+ while (true) {
139
+ const { done, value } = await reader.read();
140
+ if (done)
141
+ break;
142
+ buffer += decoder.decode(value, { stream: true });
143
+ // Process complete lines from the buffer
144
+ const lines = buffer.split('\n');
145
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
146
+ for (const line of lines) {
147
+ const trimmed = line.trim();
148
+ if (!trimmed)
149
+ continue;
150
+ if (trimmed.startsWith('data: ')) {
151
+ try {
152
+ const data = JSON.parse(trimmed.slice(6));
153
+ // Extract content chunk
154
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
155
+ if (text) {
156
+ collectedChunks.push(text);
157
+ yield text;
158
+ }
159
+ // Extract usage info (usually in the last chunk)
160
+ if (data.usageMetadata) {
161
+ usage = {
162
+ promptTokens: data.usageMetadata.promptTokenCount || 0,
163
+ completionTokens: data.usageMetadata.candidatesTokenCount || 0,
164
+ totalTokens: data.usageMetadata.totalTokenCount || 0,
165
+ };
166
+ }
167
+ }
168
+ catch {
169
+ // Skip invalid JSON chunks
170
+ }
171
+ }
172
+ }
173
+ }
174
+ // Flush decoder and process trailing buffered data even without trailing newline.
175
+ buffer += decoder.decode();
176
+ for (const line of buffer.split('\n')) {
177
+ const trimmed = line.trim();
178
+ if (!trimmed)
179
+ continue;
180
+ if (trimmed.startsWith('data: ')) {
181
+ try {
182
+ const data = JSON.parse(trimmed.slice(6));
183
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
184
+ if (text) {
185
+ collectedChunks.push(text);
186
+ yield text;
187
+ }
188
+ if (data.usageMetadata) {
189
+ usage = {
190
+ promptTokens: data.usageMetadata.promptTokenCount || 0,
191
+ completionTokens: data.usageMetadata.candidatesTokenCount || 0,
192
+ totalTokens: data.usageMetadata.totalTokenCount || 0,
193
+ };
194
+ }
195
+ }
196
+ catch {
197
+ // Skip invalid JSON chunks
198
+ }
199
+ }
200
+ }
201
+ }
202
+ finally {
203
+ reader.releaseLock();
204
+ }
205
+ const fullContent = collectedChunks.join('');
206
+ return {
207
+ content: fullContent,
208
+ usage,
209
+ };
210
+ }
94
211
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Ollama Provider - Local LLM via Ollama
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  import type { InvokeParams, InvokeResult } from '../types.js';
@@ -9,5 +11,18 @@ export declare class OllamaProvider extends BaseProvider {
9
11
  private baseUrl;
10
12
  constructor(model?: string, baseUrl?: string);
11
13
  isConfigured(): boolean;
14
+ /**
15
+ * Ollama supports streaming.
16
+ */
17
+ supportsStreaming(): boolean;
18
+ /**
19
+ * Build request body for Ollama API
20
+ */
21
+ private buildRequestBody;
12
22
  invoke(params: InvokeParams): Promise<InvokeResult>;
23
+ /**
24
+ * Stream-based invoke using Ollama's streaming API.
25
+ * Yields content chunks as they arrive from the API.
26
+ */
27
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
13
28
  }