zoe-agent 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 (267) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/LICENSE +96 -0
  3. package/README.md +568 -0
  4. package/dist/adapters/cli/agent.d.ts +59 -0
  5. package/dist/adapters/cli/agent.js +232 -0
  6. package/dist/adapters/cli/bootstrap.d.ts +25 -0
  7. package/dist/adapters/cli/bootstrap.js +204 -0
  8. package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
  9. package/dist/adapters/cli/commands/build-registry.js +88 -0
  10. package/dist/adapters/cli/commands/clear.d.ts +7 -0
  11. package/dist/adapters/cli/commands/clear.js +10 -0
  12. package/dist/adapters/cli/commands/compact.d.ts +13 -0
  13. package/dist/adapters/cli/commands/compact.js +96 -0
  14. package/dist/adapters/cli/commands/exit.d.ts +7 -0
  15. package/dist/adapters/cli/commands/exit.js +9 -0
  16. package/dist/adapters/cli/commands/gateway.d.ts +7 -0
  17. package/dist/adapters/cli/commands/gateway.js +152 -0
  18. package/dist/adapters/cli/commands/help.d.ts +9 -0
  19. package/dist/adapters/cli/commands/help.js +12 -0
  20. package/dist/adapters/cli/commands/models.d.ts +10 -0
  21. package/dist/adapters/cli/commands/models.js +32 -0
  22. package/dist/adapters/cli/commands/registry.d.ts +70 -0
  23. package/dist/adapters/cli/commands/registry.js +111 -0
  24. package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
  25. package/dist/adapters/cli/commands/settings-utils.js +182 -0
  26. package/dist/adapters/cli/commands/settings.d.ts +9 -0
  27. package/dist/adapters/cli/commands/settings.js +395 -0
  28. package/dist/adapters/cli/commands/skills.d.ts +7 -0
  29. package/dist/adapters/cli/commands/skills.js +21 -0
  30. package/dist/adapters/cli/config-loader.d.ts +27 -0
  31. package/dist/adapters/cli/config-loader.js +48 -0
  32. package/dist/adapters/cli/docker-utils.d.ts +37 -0
  33. package/dist/adapters/cli/docker-utils.js +90 -0
  34. package/dist/adapters/cli/index.d.ts +2 -0
  35. package/dist/adapters/cli/index.js +88 -0
  36. package/dist/adapters/cli/repl.d.ts +22 -0
  37. package/dist/adapters/cli/repl.js +256 -0
  38. package/dist/adapters/cli/setup.d.ts +19 -0
  39. package/dist/adapters/cli/setup.js +613 -0
  40. package/dist/adapters/cli/system-prompts.d.ts +56 -0
  41. package/dist/adapters/cli/system-prompts.js +131 -0
  42. package/dist/adapters/cli/tui/app.d.ts +58 -0
  43. package/dist/adapters/cli/tui/app.js +314 -0
  44. package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
  45. package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
  46. package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
  47. package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
  48. package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
  49. package/dist/adapters/cli/tui/components/command-palette.js +50 -0
  50. package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
  51. package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
  52. package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
  53. package/dist/adapters/cli/tui/components/error-message.js +8 -0
  54. package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
  55. package/dist/adapters/cli/tui/components/footer.js +19 -0
  56. package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
  57. package/dist/adapters/cli/tui/components/goal-status.js +22 -0
  58. package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
  59. package/dist/adapters/cli/tui/components/info-message.js +8 -0
  60. package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
  61. package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
  62. package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
  63. package/dist/adapters/cli/tui/components/markdown.js +92 -0
  64. package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
  65. package/dist/adapters/cli/tui/components/message-area.js +55 -0
  66. package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
  67. package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
  68. package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
  69. package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
  70. package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
  71. package/dist/adapters/cli/tui/components/text-input.js +142 -0
  72. package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
  73. package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
  74. package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
  75. package/dist/adapters/cli/tui/components/user-message.js +8 -0
  76. package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
  77. package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
  78. package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
  79. package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
  80. package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
  81. package/dist/adapters/cli/tui/feed-serializer.js +70 -0
  82. package/dist/adapters/cli/tui/file-index.d.ts +8 -0
  83. package/dist/adapters/cli/tui/file-index.js +41 -0
  84. package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
  85. package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
  86. package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
  87. package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
  88. package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
  89. package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
  90. package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
  91. package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
  92. package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
  93. package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
  94. package/dist/adapters/cli/tui/index.d.ts +19 -0
  95. package/dist/adapters/cli/tui/index.js +206 -0
  96. package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
  97. package/dist/adapters/cli/tui/ink-reset.js +57 -0
  98. package/dist/adapters/cli/tui/layout.d.ts +15 -0
  99. package/dist/adapters/cli/tui/layout.js +15 -0
  100. package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
  101. package/dist/adapters/cli/tui/logo/gradient.js +31 -0
  102. package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
  103. package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
  104. package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
  105. package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
  106. package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
  107. package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
  108. package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
  109. package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
  110. package/dist/adapters/cli/tui/session-export.d.ts +21 -0
  111. package/dist/adapters/cli/tui/session-export.js +63 -0
  112. package/dist/adapters/cli/tui/theme.d.ts +23 -0
  113. package/dist/adapters/cli/tui/theme.js +22 -0
  114. package/dist/adapters/cli/tui/types.d.ts +52 -0
  115. package/dist/adapters/cli/tui/types.js +12 -0
  116. package/dist/adapters/sdk/agent.d.ts +20 -0
  117. package/dist/adapters/sdk/agent.js +356 -0
  118. package/dist/adapters/sdk/http.d.ts +43 -0
  119. package/dist/adapters/sdk/http.js +61 -0
  120. package/dist/adapters/sdk/index.d.ts +58 -0
  121. package/dist/adapters/sdk/index.js +209 -0
  122. package/dist/adapters/sdk/settings.d.ts +18 -0
  123. package/dist/adapters/sdk/settings.js +57 -0
  124. package/dist/adapters/sdk/tools.d.ts +7 -0
  125. package/dist/adapters/sdk/tools.js +13 -0
  126. package/dist/adapters/server/auth.d.ts +53 -0
  127. package/dist/adapters/server/auth.js +168 -0
  128. package/dist/adapters/server/index.d.ts +40 -0
  129. package/dist/adapters/server/index.js +255 -0
  130. package/dist/adapters/server/rest-gateway.d.ts +13 -0
  131. package/dist/adapters/server/rest-gateway.js +218 -0
  132. package/dist/adapters/server/rest.d.ts +37 -0
  133. package/dist/adapters/server/rest.js +341 -0
  134. package/dist/adapters/server/server-core.d.ts +55 -0
  135. package/dist/adapters/server/server-core.js +121 -0
  136. package/dist/adapters/server/session-store.d.ts +81 -0
  137. package/dist/adapters/server/session-store.js +272 -0
  138. package/dist/adapters/server/settings-handlers.d.ts +24 -0
  139. package/dist/adapters/server/settings-handlers.js +360 -0
  140. package/dist/adapters/server/standalone.d.ts +19 -0
  141. package/dist/adapters/server/standalone.js +113 -0
  142. package/dist/adapters/server/websocket.d.ts +26 -0
  143. package/dist/adapters/server/websocket.js +68 -0
  144. package/dist/adapters/server/ws-handlers.d.ts +32 -0
  145. package/dist/adapters/server/ws-handlers.js +523 -0
  146. package/dist/adapters/server/ws-types.d.ts +304 -0
  147. package/dist/adapters/server/ws-types.js +7 -0
  148. package/dist/core/agent-loop.d.ts +68 -0
  149. package/dist/core/agent-loop.js +423 -0
  150. package/dist/core/config.d.ts +115 -0
  151. package/dist/core/config.js +189 -0
  152. package/dist/core/errors.d.ts +58 -0
  153. package/dist/core/errors.js +88 -0
  154. package/dist/core/hooks.d.ts +35 -0
  155. package/dist/core/hooks.js +49 -0
  156. package/dist/core/index.d.ts +23 -0
  157. package/dist/core/index.js +29 -0
  158. package/dist/core/message-convert.d.ts +41 -0
  159. package/dist/core/message-convert.js +94 -0
  160. package/dist/core/middleware/auth.d.ts +24 -0
  161. package/dist/core/middleware/auth.js +28 -0
  162. package/dist/core/middleware/logging.d.ts +23 -0
  163. package/dist/core/middleware/logging.js +28 -0
  164. package/dist/core/middleware/rate-limit.d.ts +27 -0
  165. package/dist/core/middleware/rate-limit.js +38 -0
  166. package/dist/core/middleware/semantic-tools.d.ts +10 -0
  167. package/dist/core/middleware/semantic-tools.js +43 -0
  168. package/dist/core/middleware.d.ts +48 -0
  169. package/dist/core/middleware.js +38 -0
  170. package/dist/core/permission.d.ts +25 -0
  171. package/dist/core/permission.js +50 -0
  172. package/dist/core/provider-config.d.ts +129 -0
  173. package/dist/core/provider-config.js +273 -0
  174. package/dist/core/provider-env.d.ts +39 -0
  175. package/dist/core/provider-env.js +142 -0
  176. package/dist/core/provider-resolver.d.ts +12 -0
  177. package/dist/core/provider-resolver.js +12 -0
  178. package/dist/core/session-store.d.ts +75 -0
  179. package/dist/core/session-store.js +245 -0
  180. package/dist/core/settings-manager.d.ts +57 -0
  181. package/dist/core/settings-manager.js +359 -0
  182. package/dist/core/settings-schema.d.ts +38 -0
  183. package/dist/core/settings-schema.js +171 -0
  184. package/dist/core/skill-catalog.d.ts +6 -0
  185. package/dist/core/skill-catalog.js +17 -0
  186. package/dist/core/skill-invoker.d.ts +127 -0
  187. package/dist/core/skill-invoker.js +182 -0
  188. package/dist/core/stream-accumulator.d.ts +21 -0
  189. package/dist/core/stream-accumulator.js +51 -0
  190. package/dist/core/stream-manager.d.ts +58 -0
  191. package/dist/core/stream-manager.js +212 -0
  192. package/dist/core/tool-executor.d.ts +84 -0
  193. package/dist/core/tool-executor.js +256 -0
  194. package/dist/core/types.d.ts +259 -0
  195. package/dist/core/types.js +11 -0
  196. package/dist/gateway/gateway.d.ts +52 -0
  197. package/dist/gateway/gateway.js +537 -0
  198. package/dist/gateway/index.d.ts +21 -0
  199. package/dist/gateway/index.js +31 -0
  200. package/dist/gateway/openapi-importer.d.ts +15 -0
  201. package/dist/gateway/openapi-importer.js +66 -0
  202. package/dist/gateway/semantic-scorer.d.ts +7 -0
  203. package/dist/gateway/semantic-scorer.js +24 -0
  204. package/dist/gateway/settings-adapter.d.ts +49 -0
  205. package/dist/gateway/settings-adapter.js +137 -0
  206. package/dist/gateway/tool-factory.d.ts +9 -0
  207. package/dist/gateway/tool-factory.js +414 -0
  208. package/dist/gateway/types.d.ts +68 -0
  209. package/dist/gateway/types.js +7 -0
  210. package/dist/models-catalog.js +46 -0
  211. package/dist/providers/anthropic.d.ts +22 -0
  212. package/dist/providers/anthropic.js +148 -0
  213. package/dist/providers/factory.d.ts +10 -0
  214. package/dist/providers/factory.js +25 -0
  215. package/dist/providers/openai.d.ts +15 -0
  216. package/dist/providers/openai.js +71 -0
  217. package/dist/providers/types.d.ts +48 -0
  218. package/dist/providers/types.js +1 -0
  219. package/dist/skills/args.d.ts +37 -0
  220. package/dist/skills/args.js +99 -0
  221. package/dist/skills/index.d.ts +11 -0
  222. package/dist/skills/index.js +23 -0
  223. package/dist/skills/loader.d.ts +3 -0
  224. package/dist/skills/loader.js +59 -0
  225. package/dist/skills/parser.d.ts +7 -0
  226. package/dist/skills/parser.js +152 -0
  227. package/dist/skills/registry.d.ts +13 -0
  228. package/dist/skills/registry.js +74 -0
  229. package/dist/skills/resolver.d.ts +19 -0
  230. package/dist/skills/resolver.js +116 -0
  231. package/dist/skills/types.d.ts +74 -0
  232. package/dist/skills/types.js +50 -0
  233. package/dist/tools/browser.d.ts +2 -0
  234. package/dist/tools/browser.js +68 -0
  235. package/dist/tools/core.d.ts +20 -0
  236. package/dist/tools/core.js +244 -0
  237. package/dist/tools/email.d.ts +2 -0
  238. package/dist/tools/email.js +61 -0
  239. package/dist/tools/image.d.ts +2 -0
  240. package/dist/tools/image.js +257 -0
  241. package/dist/tools/index.d.ts +2 -0
  242. package/dist/tools/index.js +88 -0
  243. package/dist/tools/interface.d.ts +22 -0
  244. package/dist/tools/interface.js +1 -0
  245. package/dist/tools/notify.d.ts +2 -0
  246. package/dist/tools/notify.js +100 -0
  247. package/dist/tools/prompt-optimizer.d.ts +2 -0
  248. package/dist/tools/prompt-optimizer.js +65 -0
  249. package/dist/tools/screenshot.d.ts +2 -0
  250. package/dist/tools/screenshot.js +184 -0
  251. package/dist/tools/search.d.ts +2 -0
  252. package/dist/tools/search.js +78 -0
  253. package/dist/tools/todos.d.ts +10 -0
  254. package/dist/tools/todos.js +50 -0
  255. package/package.json +119 -0
  256. package/skills/docker-ops/SKILL.md +329 -0
  257. package/skills/k8s-deploy/SKILL.md +397 -0
  258. package/skills/log-analyzer/SKILL.md +331 -0
  259. package/skills/speckit-analyze/SKILL.md +260 -0
  260. package/skills/speckit-checklist/SKILL.md +374 -0
  261. package/skills/speckit-clarify/SKILL.md +286 -0
  262. package/skills/speckit-constitution/SKILL.md +157 -0
  263. package/skills/speckit-implement/SKILL.md +224 -0
  264. package/skills/speckit-plan/SKILL.md +171 -0
  265. package/skills/speckit-specify/SKILL.md +346 -0
  266. package/skills/speckit-tasks/SKILL.md +215 -0
  267. package/skills/speckit-taskstoissues/SKILL.md +107 -0
@@ -0,0 +1,423 @@
1
+ /** Zoe Core — THE Agent Loop (single implementation) */
2
+ import { generateId, now, toZoeError, messageToProviderMessage, providerToolCallToToolCall } from "./message-convert.js";
3
+ import { StreamingResponseAccumulator } from "./stream-accumulator.js";
4
+ import { executeTool, normalizeToolResult } from "./tool-executor.js";
5
+ import { compose } from "./middleware.js";
6
+ import { checkToolPermission, getToolRiskCategory } from "./permission.js";
7
+ import { getAllToolModules } from "./tool-executor.js";
8
+ import { getModelMeta } from "../models-catalog.js";
9
+ /**
10
+ * Run the Zoe agent loop - THE single implementation.
11
+ *
12
+ * This is the canonical agent loop that all other entry points (createAgent,
13
+ * generateText, streamText, CLI Agent) will delegate to. It handles:
14
+ *
15
+ * - Multi-step reasoning with tool execution
16
+ * - Provider resolution (including per-skill switching via providerFactory)
17
+ * - System prompt injection
18
+ * - Abort signal handling
19
+ * - Hook execution
20
+ * - Usage estimation
21
+ * - Structured error reporting
22
+ * - Middleware pipeline (when provided)
23
+ *
24
+ * @param options - Agent loop configuration
25
+ * @returns AgentLoopResult with messages, steps, tool calls, usage, and finish reason
26
+ */
27
+ export async function runAgentLoop(options) {
28
+ const { provider, model, messages, toolDefs, systemPrompt, skillCatalog, maxSteps, hooks, signal, config = {}, metadata = {}, onStep, providerFactory, middleware, } = options;
29
+ // ── No middleware: run loop directly (backward compatible) ────────────
30
+ if (!middleware || middleware.length === 0) {
31
+ return executeLoop(options);
32
+ }
33
+ // ── With middleware: wrap loop in pipeline ────────────────────────────
34
+ const ctx = {
35
+ requestId: generateId(),
36
+ messages,
37
+ provider,
38
+ model,
39
+ toolDefs,
40
+ metadata,
41
+ signal,
42
+ startedAt: Date.now(),
43
+ };
44
+ try {
45
+ await compose(middleware)(ctx, async () => {
46
+ // Rebuild options from ctx to capture middleware mutations (e.g., injected tools)
47
+ const mergedOptions = {
48
+ ...options,
49
+ toolDefs: ctx.toolDefs,
50
+ config: {
51
+ ...options.config,
52
+ agentName: options.config?.agentName ?? 'zoe',
53
+ ...(ctx.metadata.injectedTools ? { injectedTools: ctx.metadata.injectedTools } : {}),
54
+ },
55
+ };
56
+ const result = await executeLoop(mergedOptions);
57
+ ctx.result = {
58
+ messages: result.messages,
59
+ steps: result.steps,
60
+ toolCalls: result.toolCalls,
61
+ usage: result.usage,
62
+ finishReason: result.finishReason,
63
+ };
64
+ });
65
+ // ctx.result is populated by the final handler
66
+ if (ctx.result) {
67
+ return {
68
+ messages: ctx.result.messages,
69
+ steps: ctx.result.steps,
70
+ toolCalls: ctx.result.toolCalls,
71
+ usage: ctx.result.usage,
72
+ finishReason: ctx.result.finishReason,
73
+ };
74
+ }
75
+ // Middleware completed without populating result (shouldn't happen)
76
+ return {
77
+ messages,
78
+ steps: [],
79
+ toolCalls: [],
80
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0 },
81
+ finishReason: "error",
82
+ error: {
83
+ message: "Middleware completed without producing a result",
84
+ code: "MIDDLEWARE_ERROR",
85
+ retryable: false,
86
+ },
87
+ };
88
+ }
89
+ catch (err) {
90
+ // Log the error for audit trail even though middleware chain was interrupted
91
+ console.error(`[middleware] request ${ctx.requestId} failed after ${Date.now() - ctx.startedAt}ms:`, err instanceof Error ? err.message : String(err));
92
+ return {
93
+ messages,
94
+ steps: [],
95
+ toolCalls: [],
96
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0 },
97
+ finishReason: "error",
98
+ error: {
99
+ message: err instanceof Error ? err.message : String(err),
100
+ code: err?.code ?? "MIDDLEWARE_ERROR",
101
+ retryable: false,
102
+ },
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Execute the core agent loop (no middleware wrapping).
108
+ * Extracted from runAgentLoop for clarity.
109
+ */
110
+ async function executeLoop(options) {
111
+ const { provider, model, messages, toolDefs, systemPrompt, skillCatalog, maxSteps, hooks, signal, config = {}, onStep, stream, providerFactory, } = options;
112
+ // Destructure approveTool outside the loop for closure access
113
+ const approveTool = options.approveTool;
114
+ const permissionLevel = options.permissionLevel;
115
+ const autoConfirm = options.autoConfirm;
116
+ // Prepend system prompt if provided and messages[0] is not already a system message
117
+ if (systemPrompt && messages.length > 0 && messages[0].role !== "system") {
118
+ messages.unshift({
119
+ id: generateId(),
120
+ role: "system",
121
+ content: systemPrompt,
122
+ timestamp: now(),
123
+ });
124
+ }
125
+ // Append skill catalog to existing system message
126
+ if (skillCatalog && messages.length > 0 && messages[0].role === 'system') {
127
+ messages[0] = { ...messages[0], content: messages[0].content + '\n\n' + skillCatalog };
128
+ }
129
+ const steps = [];
130
+ const allToolCalls = [];
131
+ let finishReason = "stop";
132
+ let loopError;
133
+ // For usage calculation
134
+ let totalPromptChars = 0;
135
+ let totalCompletionChars = 0;
136
+ // Track current provider (may change per step if providerFactory is used)
137
+ let currentProvider = provider;
138
+ let currentModel = model;
139
+ // Track whether the loop exhausted maxSteps
140
+ let hitMaxSteps = false;
141
+ for (let step = 0; step < maxSteps; step++) {
142
+ try {
143
+ // Check abort
144
+ if (signal?.aborted) {
145
+ finishReason = "aborted";
146
+ loopError = {
147
+ message: "Operation was aborted",
148
+ code: "ABORTED",
149
+ retryable: false,
150
+ };
151
+ break;
152
+ }
153
+ // Resolve provider for this step (for skill-driven provider switching)
154
+ if (providerFactory) {
155
+ try {
156
+ const resolved = await providerFactory.resolve();
157
+ currentProvider = resolved.provider;
158
+ currentModel = resolved.model;
159
+ }
160
+ catch (err) {
161
+ finishReason = "error";
162
+ loopError = {
163
+ message: err instanceof Error ? err.message : String(err),
164
+ code: "PROVIDER_ERROR",
165
+ retryable: true,
166
+ provider: currentModel,
167
+ };
168
+ const zoeErr = toZoeError(err, "PROVIDER_ERROR");
169
+ await hooks.onError(zoeErr);
170
+ break;
171
+ }
172
+ }
173
+ // Convert messages to provider format
174
+ const providerMessages = messages.map(messageToProviderMessage);
175
+ // Call provider (stream if available, else chat). Streaming emits
176
+ // text_delta steps as tokens arrive; non-streaming emits one complete
177
+ // 'text' step below. Tool calls are reassembled by the accumulator.
178
+ let response;
179
+ let streamed = false;
180
+ try {
181
+ if (stream && typeof currentProvider.chatStream === 'function') {
182
+ streamed = true;
183
+ const acc = new StreamingResponseAccumulator();
184
+ for await (const delta of currentProvider.chatStream(providerMessages, toolDefs, { signal })) {
185
+ if (delta.type === 'text_delta' && delta.content) {
186
+ acc.appendText(delta.content);
187
+ const deltaStep = { type: 'text_delta', content: delta.content, timestamp: now() };
188
+ steps.push(deltaStep);
189
+ await hooks.onStep(deltaStep);
190
+ if (onStep)
191
+ onStep(deltaStep);
192
+ }
193
+ else if (delta.type === 'tool_call_begin') {
194
+ acc.beginToolCall(delta.index, delta.id, delta.name);
195
+ }
196
+ else if (delta.type === 'tool_call_delta') {
197
+ acc.appendToolCallArgs(delta.index, delta.argumentsDelta);
198
+ }
199
+ else if (delta.type === 'finish' && delta.usage) {
200
+ acc.setUsage(delta.usage);
201
+ }
202
+ }
203
+ response = acc.toResponse();
204
+ }
205
+ else {
206
+ response = await currentProvider.chat(providerMessages, toolDefs, { signal });
207
+ }
208
+ }
209
+ catch (err) {
210
+ finishReason = "error";
211
+ const zoeErr = toZoeError(err, "PROVIDER_ERROR");
212
+ loopError = {
213
+ message: zoeErr.message,
214
+ code: "PROVIDER_ERROR",
215
+ retryable: zoeErr.retryable,
216
+ provider: currentModel,
217
+ };
218
+ await hooks.onError(zoeErr);
219
+ break;
220
+ }
221
+ // Track prompt chars for usage
222
+ for (const msg of providerMessages) {
223
+ totalPromptChars += (msg.content ?? "").length;
224
+ }
225
+ // Text content. When streamed, tokens already went out as text_delta steps,
226
+ // so we only emit the complete 'text' step for the non-streamed path; the
227
+ // assembled content is always added to history either way.
228
+ if (response.content) {
229
+ totalCompletionChars += response.content.length;
230
+ if (!streamed) {
231
+ const textStep = {
232
+ type: "text",
233
+ content: response.content,
234
+ timestamp: now(),
235
+ };
236
+ steps.push(textStep);
237
+ await hooks.onStep(textStep);
238
+ if (onStep)
239
+ onStep(textStep);
240
+ }
241
+ // Add assistant message with text content
242
+ messages.push({
243
+ id: generateId(),
244
+ role: "assistant",
245
+ content: response.content,
246
+ timestamp: now(),
247
+ });
248
+ }
249
+ // Tool calls
250
+ if (response.tool_calls && response.tool_calls.length > 0) {
251
+ const assistantToolCalls = response.tool_calls.map(providerToolCallToToolCall);
252
+ allToolCalls.push(...assistantToolCalls);
253
+ // Add assistant message with tool calls
254
+ const assistantMsg = {
255
+ id: generateId(),
256
+ role: "assistant",
257
+ content: response.content ?? "",
258
+ toolCalls: assistantToolCalls,
259
+ timestamp: now(),
260
+ };
261
+ messages.push(assistantMsg);
262
+ // Execute each tool call
263
+ for (const tc of response.tool_calls) {
264
+ if (signal?.aborted) {
265
+ finishReason = "aborted";
266
+ loopError = {
267
+ message: "Operation was aborted during tool execution",
268
+ code: "ABORTED",
269
+ retryable: false,
270
+ };
271
+ break;
272
+ }
273
+ let parsedArgs;
274
+ try {
275
+ parsedArgs = JSON.parse(tc.arguments);
276
+ }
277
+ catch {
278
+ parsedArgs = { raw: tc.arguments };
279
+ }
280
+ await hooks.beforeToolCall({ name: tc.name, args: parsedArgs });
281
+ // Forward a tool's live progress (e.g. streaming shell stdout) to the
282
+ // adapter as a tool_progress step. Emitted via onStep only — not pushed
283
+ // to result.steps (chunks are transient presentation, not semantic).
284
+ const onUpdate = (progress) => {
285
+ if (progress.message != null && onStep) {
286
+ onStep({
287
+ type: "tool_progress",
288
+ toolCallId: tc.id,
289
+ name: tc.name,
290
+ args: parsedArgs,
291
+ content: progress.message,
292
+ timestamp: now(),
293
+ });
294
+ }
295
+ };
296
+ const execExtra = { onUpdate, signal };
297
+ // Check for dynamically injected tools (from semantic middleware)
298
+ const injectedTools = config?.injectedTools;
299
+ const injectedModule = injectedTools instanceof Map ? injectedTools.get(tc.name) : undefined;
300
+ const start = now();
301
+ let output;
302
+ let metadata;
303
+ // Runs the tool (injected module or registry), normalizing both branches
304
+ // into { output, metadata } and turning throws into an error output.
305
+ // Shared by all three permission paths below so the try/catch lives once.
306
+ const runToolSafely = async () => {
307
+ try {
308
+ const result = injectedModule
309
+ ? normalizeToolResult(await injectedModule.handler(parsedArgs, config))
310
+ : await executeTool(tc.name, parsedArgs, config, execExtra);
311
+ return { output: result.output, metadata: result.metadata };
312
+ }
313
+ catch (err) {
314
+ return { output: `Error: ${err instanceof Error ? err.message : String(err)}` };
315
+ }
316
+ };
317
+ // Permission pre-filter + adapter-level tool approval
318
+ const effectiveLevel = permissionLevel ?? "moderate";
319
+ if (autoConfirm) {
320
+ // --headless mode: bypass permission matrix, auto-approve everything
321
+ ({ output, metadata } = await runToolSafely());
322
+ }
323
+ else {
324
+ const riskCategory = injectedModule?.risk
325
+ ?? getToolRiskCategory(tc.name, getAllToolModules());
326
+ const decision = checkToolPermission(effectiveLevel, riskCategory);
327
+ if (decision === "auto") {
328
+ ({ output, metadata } = await runToolSafely());
329
+ }
330
+ else if (approveTool) {
331
+ let approved;
332
+ try {
333
+ approved = await approveTool({ name: tc.name, args: parsedArgs });
334
+ }
335
+ catch {
336
+ approved = false;
337
+ }
338
+ if (!approved) {
339
+ output = "User denied tool execution.";
340
+ }
341
+ else {
342
+ ({ output, metadata } = await runToolSafely());
343
+ }
344
+ }
345
+ else {
346
+ output = "Tool execution denied.";
347
+ }
348
+ }
349
+ const duration = now() - start;
350
+ // Note: tool output chars are NOT counted here because they will be
351
+ // counted as promptChars on the next loop iteration when the message
352
+ // history (including tool results) is sent to the provider.
353
+ // Add tool result message
354
+ messages.push({
355
+ id: generateId(),
356
+ role: "tool",
357
+ content: output,
358
+ toolCallId: tc.id,
359
+ timestamp: now(),
360
+ });
361
+ // Record step
362
+ const toolStep = {
363
+ type: "tool_call",
364
+ toolCall: {
365
+ id: tc.id,
366
+ name: tc.name,
367
+ args: parsedArgs,
368
+ result: output,
369
+ duration,
370
+ },
371
+ metadata,
372
+ timestamp: now(),
373
+ };
374
+ steps.push(toolStep);
375
+ await hooks.onStep(toolStep);
376
+ await hooks.afterToolCall({ name: tc.name, output, duration });
377
+ if (onStep)
378
+ onStep(toolStep);
379
+ }
380
+ if (finishReason === "aborted")
381
+ break;
382
+ // Continue the loop to get the next response
383
+ // Mark if this was the last allowed iteration
384
+ if (step + 1 >= maxSteps) {
385
+ hitMaxSteps = true;
386
+ }
387
+ continue;
388
+ }
389
+ // No tool calls — we're done
390
+ finishReason = "stop";
391
+ break;
392
+ }
393
+ finally {
394
+ if (providerFactory)
395
+ providerFactory.restore();
396
+ }
397
+ }
398
+ // The loop ran all iterations with tool calls on the last one
399
+ if (hitMaxSteps) {
400
+ finishReason = "max_steps";
401
+ }
402
+ // Calculate usage
403
+ const promptTokens = Math.ceil(totalPromptChars / 4);
404
+ const completionTokens = Math.ceil(totalCompletionChars / 4);
405
+ const pricing = getModelMeta(currentModel)?.pricing;
406
+ const cost = pricing
407
+ ? (promptTokens * pricing.input + completionTokens * pricing.output) / 1_000_000
408
+ : 0;
409
+ const usage = {
410
+ promptTokens,
411
+ completionTokens,
412
+ totalTokens: promptTokens + completionTokens,
413
+ cost,
414
+ };
415
+ return {
416
+ messages,
417
+ steps,
418
+ toolCalls: allToolCalls,
419
+ usage,
420
+ finishReason,
421
+ error: loopError,
422
+ };
423
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Zoe Core — Config Utilities
3
+ *
4
+ * Config loading, merging, and environment overrides.
5
+ * Chalk-free — suitable for all adapters (CLI, SDK, Server).
6
+ */
7
+ import { ProviderType } from '../providers/types.js';
8
+ export interface AppConfig {
9
+ provider?: ProviderType;
10
+ apiKey?: string;
11
+ baseUrl?: string;
12
+ model?: string;
13
+ models?: {
14
+ 'openai-compatible'?: {
15
+ apiKey: string;
16
+ baseUrl: string;
17
+ model: string;
18
+ };
19
+ openai?: {
20
+ apiKey: string;
21
+ model: string;
22
+ };
23
+ anthropic?: {
24
+ apiKey: string;
25
+ model: string;
26
+ };
27
+ glm?: {
28
+ apiKey: string;
29
+ model: string;
30
+ };
31
+ };
32
+ imageApiKey?: string;
33
+ imageBaseUrl?: string;
34
+ imageModel?: string;
35
+ imageSize?: string;
36
+ imageQuality?: string;
37
+ imageStyle?: string;
38
+ imageN?: number;
39
+ smtpHost?: string;
40
+ smtpPort?: string;
41
+ smtpUser?: string;
42
+ smtpPass?: string;
43
+ smtpFrom?: string;
44
+ tavilyApiKey?: string;
45
+ autoConfirm?: boolean;
46
+ permissionLevel?: "strict" | "moderate" | "permissive";
47
+ feishuWebhook?: string;
48
+ feishuKeyword?: string;
49
+ dingtalkWebhook?: string;
50
+ dingtalkKeyword?: string;
51
+ wecomWebhook?: string;
52
+ wecomKeyword?: string;
53
+ }
54
+ /**
55
+ * Returns the config file path for the given scope.
56
+ */
57
+ export declare function getConfigPath(global?: boolean): string;
58
+ /**
59
+ * Returns the config directory path for the given scope.
60
+ */
61
+ export declare function getConfigDir(global?: boolean): string;
62
+ /**
63
+ * Returns both global and local config paths.
64
+ */
65
+ export declare function getConfigPaths(): {
66
+ global: string;
67
+ local: string;
68
+ globalDir: string;
69
+ };
70
+ /**
71
+ * Load and parse a JSON config file.
72
+ * Returns `{ config, warning }` — warning is set if parsing failed.
73
+ */
74
+ export declare function loadJsonConfig(filePath: string): {
75
+ config: AppConfig;
76
+ warning?: string;
77
+ };
78
+ /**
79
+ * Load global and local configs and merge them.
80
+ * Priority: local > global.
81
+ */
82
+ export declare function loadMergedConfig(): AppConfig;
83
+ /**
84
+ * Apply environment variable overrides to the merged config.
85
+ * Env vars take priority over JSON config for tool settings.
86
+ * Also injects provider API keys from env vars into the models map.
87
+ */
88
+ export declare function applyEnvOverrides(config: AppConfig): AppConfig;
89
+ /**
90
+ * Auto-migrate legacy config format (top-level apiKey/baseUrl/model) to the
91
+ * models map format used by the current architecture.
92
+ */
93
+ export declare function migrateLegacyFormat(config: AppConfig, options?: {
94
+ model?: string;
95
+ }): AppConfig;
96
+ /**
97
+ * Resolve the active provider type from CLI flags, env vars, and config.
98
+ * Checks LLM_PROVIDER env var as a standard alias for ZOE_PROVIDER.
99
+ */
100
+ export declare function resolveActiveProviderType(config: AppConfig, options?: {
101
+ provider?: string;
102
+ }): ProviderType;
103
+ /**
104
+ * Save config to disk. If a local config exists, saves there; otherwise global.
105
+ */
106
+ export declare function saveConfig(config: AppConfig): void;
107
+ /**
108
+ * Save config to a specific path.
109
+ * Throws on failure — callers handle error display.
110
+ */
111
+ export declare function writeConfigToPath(config: AppConfig, targetFile: string): void;
112
+ /**
113
+ * Mask a secret string for display, showing only first 3 and last 4 chars.
114
+ */
115
+ export declare function maskSecret(secret?: string): string;