@agile-vibe-coding/avc 0.1.1 → 0.3.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 (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,493 @@
1
+ import OpenAI from 'openai';
2
+ import { jsonrepair } from 'jsonrepair';
3
+ import { LLMProvider } from './llm-provider.js';
4
+
5
+ /**
6
+ * Known local inference servers and their default endpoints.
7
+ * All expose an OpenAI-compatible Chat Completions API.
8
+ */
9
+ const KNOWN_SERVERS = [
10
+ { app: 'lmstudio', url: 'http://localhost:1234/v1', label: 'LM Studio' },
11
+ { app: 'ollama', url: 'http://localhost:11434/v1', label: 'Ollama' },
12
+ { app: 'llamacpp', url: 'http://localhost:8080/v1', label: 'llama.cpp' },
13
+ { app: 'vllm', url: 'http://localhost:8000/v1', label: 'vLLM' },
14
+ { app: 'localai', url: 'http://localhost:8081/v1', label: 'LocalAI' },
15
+ ];
16
+
17
+ export { KNOWN_SERVERS };
18
+
19
+ /**
20
+ * Probe a single server: GET /models with a short timeout.
21
+ * @param {string} baseURL - e.g. 'http://localhost:1234/v1'
22
+ * @param {number} timeoutMs
23
+ * @returns {Promise<Array<{id:string, owned_by?:string, context_length?:number}>>}
24
+ */
25
+ async function probeServer(baseURL, timeoutMs = 1500) {
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
28
+ try {
29
+ const resp = await fetch(`${baseURL}/models`, { signal: controller.signal });
30
+ if (!resp.ok) return [];
31
+ const body = await resp.json();
32
+ return body.data || [];
33
+ } catch {
34
+ return [];
35
+ } finally {
36
+ clearTimeout(timer);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Discover all running local inference servers and their loaded models.
42
+ * Also checks LOCAL_LLM_URL env var for custom endpoints.
43
+ * @returns {Promise<Array<{app:string, label:string, url:string, models:Array}>>}
44
+ */
45
+ export async function discoverLocalServers() {
46
+ const servers = [...KNOWN_SERVERS];
47
+
48
+ // Add custom endpoint from env if set and not already in the list
49
+ const customUrl = process.env.LOCAL_LLM_URL;
50
+ if (customUrl) {
51
+ const normalized = customUrl.replace(/\/+$/, '');
52
+ const base = normalized.endsWith('/v1') ? normalized : `${normalized}/v1`;
53
+ if (!servers.some(s => s.url === base)) {
54
+ servers.unshift({ app: 'custom', url: base, label: 'Custom Local' });
55
+ }
56
+ }
57
+
58
+ const results = await Promise.all(
59
+ servers.map(async (server) => {
60
+ const rawModels = await probeServer(server.url);
61
+ if (rawModels.length === 0) return null;
62
+ const models = rawModels.map(m => ({
63
+ id: m.id,
64
+ ownedBy: m.owned_by || null,
65
+ contextLength: m.context_length || null,
66
+ }));
67
+ return { ...server, models };
68
+ })
69
+ );
70
+
71
+ return results.filter(Boolean);
72
+ }
73
+
74
+ /**
75
+ * Strip any XML-like tag blocks that local models use for internal reasoning.
76
+ * Models use a variety of tags: <think>, <reasoning>, <reflection>, <scratchpad>,
77
+ * <internal>, <thought>, <analysis>, <planning>, <chain_of_thought>, etc.
78
+ * Instead of maintaining an allowlist, we strip ALL matched <word>...</word> blocks
79
+ * that appear before the actual content.
80
+ */
81
+ function cleanLocalResponse(text) {
82
+ let cleaned = text;
83
+
84
+ // Pass 1: remove all <single_word>...</single_word> blocks (case-insensitive, multi-line).
85
+ // These are almost universally reasoning/thinking wrappers in local models.
86
+ // Matches tags like <think>, <reasoning>, <reflection>, <scratchpad>, <thought>,
87
+ // <analysis>, <planning>, <internal>, <chain_of_thought>, etc.
88
+ // Uses a backreference to ensure opening and closing tags match.
89
+ cleaned = cleaned.replace(/<([a-z][a-z0-9_]*)>[\s\S]*?<\/\1>\s*/gi, '');
90
+
91
+ return cleaned.trim();
92
+ }
93
+
94
+ /**
95
+ * For JSON responses: extract the JSON payload from whatever the model emitted.
96
+ * Handles code fences, residual text before/after JSON, and nested structures.
97
+ */
98
+ function extractJSON(text) {
99
+ let s = text.trim();
100
+
101
+ // Strip markdown code fences
102
+ if (s.startsWith('```')) {
103
+ s = s.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?\s*```\s*$/, '').trim();
104
+ }
105
+
106
+ // If it already looks like JSON, return as-is
107
+ if (s.startsWith('{') || s.startsWith('[')) return s;
108
+
109
+ // Otherwise, find the first { or [ and extract to its matching closer.
110
+ // This handles models that emit preamble text before the JSON.
111
+ const objStart = s.indexOf('{');
112
+ const arrStart = s.indexOf('[');
113
+ let start = -1;
114
+ let openChar, closeChar;
115
+
116
+ if (objStart >= 0 && (arrStart < 0 || objStart < arrStart)) {
117
+ start = objStart; openChar = '{'; closeChar = '}';
118
+ } else if (arrStart >= 0) {
119
+ start = arrStart; openChar = '['; closeChar = ']';
120
+ }
121
+
122
+ if (start < 0) return s; // no JSON found, return as-is for error reporting
123
+
124
+ // Walk forward counting nesting depth, respecting strings
125
+ let depth = 0;
126
+ let inString = false;
127
+ let escape = false;
128
+ for (let i = start; i < s.length; i++) {
129
+ const ch = s[i];
130
+ if (escape) { escape = false; continue; }
131
+ if (ch === '\\' && inString) { escape = true; continue; }
132
+ if (ch === '"') { inString = !inString; continue; }
133
+ if (inString) continue;
134
+ if (ch === openChar) depth++;
135
+ if (ch === closeChar) { depth--; if (depth === 0) return s.slice(start, i + 1); }
136
+ }
137
+
138
+ // Unbalanced — return from start to end, let JSON.parse report the real error
139
+ return s.slice(start);
140
+ }
141
+
142
+ /**
143
+ * LocalProvider — connects to any OpenAI-compatible local inference server.
144
+ * Uses the `openai` SDK pointed at a local base URL.
145
+ */
146
+ export class LocalProvider extends LLMProvider {
147
+ /**
148
+ * @param {string} model - Model ID loaded on the local server
149
+ * @param {string} [baseURL] - Override base URL (defaults to auto-detect or LOCAL_LLM_URL)
150
+ */
151
+ constructor(model = 'default', baseURL = null) {
152
+ super('local', model);
153
+ this._baseURL = baseURL || process.env.LOCAL_LLM_URL || null;
154
+ }
155
+
156
+ /**
157
+ * Auto-detect which server has the requested model, or use configured base URL.
158
+ */
159
+ async _resolveBaseURL() {
160
+ if (this._baseURL) {
161
+ const normalized = this._baseURL.replace(/\/+$/, '');
162
+ return normalized.endsWith('/v1') ? normalized : `${normalized}/v1`;
163
+ }
164
+
165
+ // Auto-detect: probe known servers for the model
166
+ const servers = await discoverLocalServers();
167
+ for (const server of servers) {
168
+ if (server.models.some(m => m.id === this.model)) {
169
+ this._baseURL = server.url;
170
+ return server.url;
171
+ }
172
+ }
173
+
174
+ // Fallback: if any server is running, use the first one
175
+ if (servers.length > 0) {
176
+ this._baseURL = servers[0].url;
177
+ return servers[0].url;
178
+ }
179
+
180
+ throw new Error(
181
+ `No local inference server found. Start LM Studio, Ollama, or another local server, ` +
182
+ `or set LOCAL_LLM_URL in your .env file.`
183
+ );
184
+ }
185
+
186
+ _createClient() {
187
+ // Deferred — actual client creation happens in _ensureClient() after async base URL resolution
188
+ return null;
189
+ }
190
+
191
+ async _ensureClient() {
192
+ if (this._client) return;
193
+ const baseURL = await this._resolveBaseURL();
194
+ this._client = new OpenAI({
195
+ baseURL,
196
+ apiKey: 'not-needed', // Local servers don't require API keys
197
+ timeout: 30 * 60_000, // 30 min — local models on consumer hardware can be very slow
198
+ maxRetries: 0,
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Stream a chat completion and collect the full response.
204
+ * Streaming prevents server-side idle timeouts (e.g. LM Studio's 5-min default)
205
+ * because tokens flow continuously over the connection.
206
+ * @returns {{ content: string, usage: object|null }}
207
+ */
208
+ async _streamCompletion(params) {
209
+ const stream = await this._client.chat.completions.create({
210
+ ...params,
211
+ stream: true,
212
+ stream_options: { include_usage: true },
213
+ });
214
+
215
+ const chunks = [];
216
+ let usage = null;
217
+ for await (const chunk of stream) {
218
+ const delta = chunk.choices?.[0]?.delta?.content;
219
+ if (delta) chunks.push(delta);
220
+ // The last chunk with stream_options.include_usage carries the usage object
221
+ if (chunk.usage) usage = chunk.usage;
222
+ }
223
+ return { content: chunks.join(''), usage };
224
+ }
225
+
226
+ async _callProvider(prompt, maxTokens, systemInstructions) {
227
+ await this._ensureClient();
228
+
229
+ const messages = [];
230
+ if (systemInstructions) {
231
+ messages.push({ role: 'system', content: systemInstructions });
232
+ }
233
+ messages.push({ role: 'user', content: prompt });
234
+
235
+ const params = {
236
+ model: this.model,
237
+ messages,
238
+ // Don't send max_tokens to local servers — let the server use its own
239
+ // context window limit. Artificially capping output truncates large responses.
240
+ };
241
+
242
+ const { content, usage } = await this._streamCompletion(params);
243
+ this._trackTokens(usage);
244
+ return cleanLocalResponse(content);
245
+ }
246
+
247
+ async generateJSON(prompt, agentInstructions = null, cachedContext = null) {
248
+ await this._ensureClient();
249
+
250
+ const JSON_SYSTEM = 'You are a helpful assistant that always returns valid JSON. Your response must be a valid JSON object or array, nothing else. Do not include any thinking, reasoning, or explanation — only the JSON.';
251
+ const systemParts = [JSON_SYSTEM];
252
+ if (agentInstructions) systemParts.push(agentInstructions);
253
+ if (cachedContext) systemParts.push(`---\n\n${cachedContext}`);
254
+
255
+ const messages = [
256
+ { role: 'system', content: systemParts.join('\n\n') },
257
+ { role: 'user', content: prompt },
258
+ ];
259
+
260
+ const params = {
261
+ model: this.model,
262
+ messages,
263
+ // Don't send max_tokens to local servers — let the server use its own
264
+ // context window limit. Artificially capping output truncates large responses.
265
+ };
266
+
267
+ // Try JSON mode — not all local servers support it; fall back gracefully
268
+ let useJsonMode = true;
269
+
270
+ const _t0 = Date.now();
271
+ let content;
272
+ let usage;
273
+ try {
274
+ ({ content, usage } = await this._withRetry(
275
+ () => this._streamCompletion({
276
+ ...params,
277
+ ...(useJsonMode ? { response_format: { type: 'json_object' } } : {}),
278
+ }),
279
+ 'JSON generation (Local)'
280
+ ));
281
+ } catch (err) {
282
+ // If JSON mode is not supported, retry without it
283
+ if (useJsonMode && (err.message?.includes('response_format') || err.status === 400)) {
284
+ useJsonMode = false;
285
+ ({ content, usage } = await this._withRetry(
286
+ () => this._streamCompletion(params),
287
+ 'JSON generation (Local, no json_mode)'
288
+ ));
289
+ } else {
290
+ throw err;
291
+ }
292
+ }
293
+
294
+ this._trackTokens(usage, {
295
+ prompt,
296
+ agentInstructions: agentInstructions ?? null,
297
+ response: content,
298
+ elapsed: Date.now() - _t0,
299
+ });
300
+
301
+ // Strip reasoning tags, code fences, and extract JSON payload
302
+ let jsonStr = extractJSON(cleanLocalResponse(content));
303
+
304
+ try {
305
+ return JSON.parse(jsonStr);
306
+ } catch (firstError) {
307
+ if (jsonStr.startsWith('{') || jsonStr.startsWith('[')) {
308
+ try { return JSON.parse(jsonrepair(jsonStr)); } catch { /* fall through */ }
309
+ }
310
+ throw new Error(`Failed to parse JSON response from local model: ${firstError.message}\n\nResponse was:\n${content}`);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Generate JSON with tool-calling support.
316
+ * Runs a tool-call loop: if the model emits tool_calls, they are executed
317
+ * via the provided dispatcher and results fed back until the model returns
318
+ * a final JSON content response.
319
+ *
320
+ * @param {string} prompt - User prompt
321
+ * @param {string|null} agentInstructions - System instructions
322
+ * @param {Array} tools - OpenAI-format tool definitions
323
+ * @param {Function} toolDispatcher - async (name, args) => string
324
+ * @param {number} [maxRounds=5] - Max tool-call rounds to prevent infinite loops
325
+ * @returns {Promise<Object>} Parsed JSON response
326
+ */
327
+ async generateJSONWithTools(prompt, agentInstructions, tools, toolDispatcher, maxRounds = 5) {
328
+ await this._ensureClient();
329
+
330
+ const JSON_SYSTEM = 'You are a helpful assistant that always returns valid JSON. When you need accurate API details for external services, use the fetch_api_reference tool before writing your response. Your final response must be a valid JSON object or array, nothing else.';
331
+ const systemParts = [JSON_SYSTEM];
332
+ if (agentInstructions) systemParts.push(agentInstructions);
333
+
334
+ const messages = [
335
+ { role: 'system', content: systemParts.join('\n\n') },
336
+ { role: 'user', content: prompt },
337
+ ];
338
+
339
+ const _t0 = Date.now();
340
+ let totalUsage = { prompt_tokens: 0, completion_tokens: 0 };
341
+ let toolCallCount = 0;
342
+
343
+ let forceNoTools = false;
344
+ for (let round = 0; round < maxRounds + 2; round++) {
345
+ const params = {
346
+ model: this.model,
347
+ messages,
348
+ ...(forceNoTools ? {} : { tools }),
349
+ };
350
+
351
+ let response;
352
+ try {
353
+ if (forceNoTools) {
354
+ // Final forced round: use streaming to prevent LM Studio's server-side idle timeout
355
+ // (non-streaming requests can timeout after ~5 min under load).
356
+ // No tools in params, so we only expect content back — streaming is safe.
357
+ const { content, usage } = await this._streamCompletion(params);
358
+ response = {
359
+ usage,
360
+ choices: [{ finish_reason: 'stop', message: { content, tool_calls: null } }],
361
+ };
362
+ } else {
363
+ // Tool-call rounds: use non-streaming (streaming tool call support is inconsistent across local servers)
364
+ response = await this._withRetry(
365
+ () => this._client.chat.completions.create(params),
366
+ `JSON+tools generation (Local, round ${round + 1})`
367
+ );
368
+ }
369
+ } catch (err) {
370
+ // If tools not supported, fall back to regular generateJSON
371
+ if (err.message?.includes('tools') || err.status === 400) {
372
+ console.log('[DEBUG] Local server does not support tools — falling back to generateJSON');
373
+ return this.generateJSON(prompt, agentInstructions);
374
+ }
375
+ throw err;
376
+ }
377
+
378
+ // Accumulate token usage
379
+ if (response.usage) {
380
+ totalUsage.prompt_tokens += response.usage.prompt_tokens || 0;
381
+ totalUsage.completion_tokens += response.usage.completion_tokens || 0;
382
+ }
383
+
384
+ const choice = response.choices?.[0];
385
+ if (!choice) {
386
+ throw new Error('No response choice from local model');
387
+ }
388
+
389
+ // If the model wants to call tools, execute them and loop
390
+ if (choice.finish_reason === 'tool_calls' || choice.message?.tool_calls?.length > 0) {
391
+ if (round >= maxRounds) {
392
+ console.log(`[DEBUG] Tool call loop exceeded ${maxRounds} rounds — forcing final response without tools`);
393
+ // Re-request without tools to force a content response
394
+ messages.push(choice.message);
395
+ messages.push({ role: 'user', content: 'Please provide your final JSON response now, without calling any more tools.' });
396
+ forceNoTools = true;
397
+ continue;
398
+ }
399
+
400
+ // Append assistant message with tool calls
401
+ messages.push(choice.message);
402
+
403
+ // Execute each tool call
404
+ for (const toolCall of choice.message.tool_calls) {
405
+ toolCallCount++;
406
+ const fnName = toolCall.function?.name;
407
+ let fnArgs;
408
+ try {
409
+ fnArgs = JSON.parse(toolCall.function?.arguments || '{}');
410
+ } catch {
411
+ fnArgs = {};
412
+ }
413
+
414
+ console.log(`[DEBUG] Tool call #${toolCallCount}: ${fnName}(${JSON.stringify(fnArgs)})`);
415
+
416
+ let result;
417
+ try {
418
+ result = await toolDispatcher(fnName, fnArgs);
419
+ } catch (err) {
420
+ result = `Tool error: ${err.message}`;
421
+ }
422
+
423
+ // Append tool result
424
+ messages.push({
425
+ role: 'tool',
426
+ tool_call_id: toolCall.id,
427
+ content: typeof result === 'string' ? result : JSON.stringify(result),
428
+ });
429
+ }
430
+
431
+ continue; // Loop back for the model to process tool results
432
+ }
433
+
434
+ // Model returned content — extract and parse JSON
435
+ const content = choice.message?.content || '';
436
+ this._trackTokens(totalUsage, {
437
+ prompt,
438
+ agentInstructions: agentInstructions ?? null,
439
+ response: content,
440
+ elapsed: Date.now() - _t0,
441
+ });
442
+
443
+ if (toolCallCount > 0) {
444
+ console.log(`[DEBUG] Tool-augmented generation complete: ${toolCallCount} tool call(s) in ${round + 1} round(s)`);
445
+ }
446
+
447
+ const jsonStr = extractJSON(cleanLocalResponse(content));
448
+ try {
449
+ return JSON.parse(jsonStr);
450
+ } catch (firstError) {
451
+ if (jsonStr.startsWith('{') || jsonStr.startsWith('[')) {
452
+ try { return JSON.parse(jsonrepair(jsonStr)); } catch { /* fall through */ }
453
+ }
454
+ throw new Error(`Failed to parse JSON response from local model (with tools): ${firstError.message}\n\nResponse was:\n${content}`);
455
+ }
456
+ }
457
+
458
+ throw new Error('Tool call loop exhausted without final response');
459
+ }
460
+
461
+ async generateText(prompt, agentInstructions = null, cachedContext = null) {
462
+ await this._ensureClient();
463
+
464
+ const systemParts = [];
465
+ if (agentInstructions) systemParts.push(agentInstructions);
466
+ if (cachedContext) systemParts.push(cachedContext);
467
+
468
+ const messages = [];
469
+ if (systemParts.length > 0) {
470
+ messages.push({ role: 'system', content: systemParts.join('\n\n') });
471
+ }
472
+ messages.push({ role: 'user', content: prompt });
473
+
474
+ const _t0 = Date.now();
475
+ const { content, usage } = await this._withRetry(
476
+ () => this._streamCompletion({
477
+ model: this.model,
478
+ messages,
479
+ // Don't send max_tokens to local servers — let the server manage limits.
480
+ }),
481
+ 'Text generation (Local)'
482
+ );
483
+
484
+ const textContent = cleanLocalResponse(content);
485
+ this._trackTokens(usage, {
486
+ prompt,
487
+ agentInstructions: agentInstructions ?? null,
488
+ response: textContent,
489
+ elapsed: Date.now() - _t0,
490
+ });
491
+ return textContent;
492
+ }
493
+ }