skimpyclaw 0.3.14 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -1,4 +1,7 @@
1
1
  // Providers Module - Unified AI Provider Interface
2
+ //
3
+ // Provider registry pattern: adapters implement ProviderAdapter (chat, chatWithTools, isAvailable).
4
+ // Routing resolves a model spec to an adapter and delegates to it.
2
5
  import { calculateUsageCost, isLangfuseEnabled } from '../langfuse.js';
3
6
  // Re-export utilities
4
7
  export { TOOL_GUARD, setUsingOAuth, isUsingOAuth, buildSystemParam, addToolCacheBreakpoint, contentToText, toOpenAITools, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, getProvider, stripProvider, buildThinkingConfig, } from './utils.js';
@@ -7,15 +10,31 @@ export { toOpenAIContent, toCodexContent, toCodexToolDefinitions } from './conte
7
10
  // Re-export observability
8
11
  export { setLangfuseHelpers, toCostDetails, toAnthropicUsageDetails, toUsageDetails, toNumericUsageDetails, } from './observability.js';
9
12
  import { setLangfuseHelpers } from './observability.js';
10
- // Import provider functions directly to avoid circular deps
11
- import { setAnthropicClient, isAnthropicAvailable, chatAnthropic, chatWithToolsAnthropic, } from './anthropic.js';
12
- import { addOpenAIClient, clearOpenAIClients, isOpenAIAvailable, chatOpenAI, chatWithToolsOpenAI, } from './openai.js';
13
- import { addResponsesApiProvider, isResponsesApiProvider, setCodexAuthPath, setCodexBaseUrl, initCodexAuth, resetCodexProviderState, isCodexAvailable, chatCodex, chatWithToolsCodex, } from './codex.js';
13
+ // Import provider module functions for init and backward-compat re-exports
14
+ import { setAnthropicClient, } from './anthropic.js';
15
+ import { addResponsesApiProvider, isResponsesApiProvider, setCodexAuthPath, setCodexBaseUrl, initCodexAuth, resetCodexProviderState, } from './codex.js';
14
16
  import { setUsingOAuth, resolveProviderRoute, shouldUseCodexAliasProvider, } from './utils.js';
15
17
  import Anthropic from '@anthropic-ai/sdk';
16
- import OpenAI from 'openai';
18
+ // Lazy adapter imports (avoid circular deps at module load time)
19
+ import { AnthropicAdapter } from './adapters/anthropic-adapter.js';
20
+ import { CodexAdapter } from './adapters/codex-adapter.js';
17
21
  // Wire provider observability helpers to runtime cost calculator.
18
22
  setLangfuseHelpers(calculateUsageCost, isLangfuseEnabled);
23
+ // ---------------------------------------------------------------------------
24
+ // Provider Registry
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Resolve a provider name to a ProviderAdapter instance.
28
+ * Adapters are lightweight — creating one per call is fine.
29
+ */
30
+ export function getAdapter(provider) {
31
+ if (provider === 'anthropic')
32
+ return new AnthropicAdapter();
33
+ // Codex providers are registered dynamically via addResponsesApiProvider
34
+ if (isResponsesApiProvider(provider))
35
+ return new CodexAdapter();
36
+ throw new Error(`Unknown provider "${provider}"`);
37
+ }
19
38
  function normalizeChatRoute(options, config) {
20
39
  const route = resolveProviderRoute(options.model, config);
21
40
  const { resolvedModel, provider, modelId, isCodexModel } = route;
@@ -27,16 +46,46 @@ function normalizeChatRoute(options, config) {
27
46
  useCodexAliasProvider: shouldUseCodexAliasProvider(provider, isCodexModel, isResponsesApiProvider('codex')),
28
47
  };
29
48
  }
30
- // Re-export all provider functions
49
+ /**
50
+ * Resolve routing and return the correct adapter + normalized options.
51
+ * Handles the Codex alias compatibility path (openai/*-codex → codex provider).
52
+ */
53
+ function resolveAdapter(options, config) {
54
+ const { resolvedModel, provider, chatOpts, useCodexAliasProvider } = normalizeChatRoute(options, config);
55
+ // Codex alias compatibility: openai/*-codex routes to codex when configured
56
+ if (useCodexAliasProvider || isResponsesApiProvider(provider)) {
57
+ const codexAdapter = new CodexAdapter();
58
+ if (codexAdapter.isAvailable()) {
59
+ return { adapter: codexAdapter, resolvedModel, chatOpts };
60
+ }
61
+ throw new Error(`Codex provider "${provider}" is configured but auth is unavailable. Run "codex" to re-authenticate.`);
62
+ }
63
+ let adapter;
64
+ try {
65
+ adapter = getAdapter(provider);
66
+ }
67
+ catch {
68
+ throw new Error(`Unknown provider "${provider}" for model: ${resolvedModel}`);
69
+ }
70
+ if (adapter.isAvailable()) {
71
+ return { adapter, resolvedModel, chatOpts };
72
+ }
73
+ throw new Error(`Unknown provider "${provider}" for model: ${resolvedModel}`);
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Backward-compat re-exports (provider module functions)
77
+ // ---------------------------------------------------------------------------
78
+ // Anthropic
31
79
  export { setAnthropicClient, isAnthropicAvailable, chatAnthropic, chatWithToolsAnthropic, } from './anthropic.js';
32
- export { addOpenAIClient, getOpenAIClient, hasOpenAIClient, clearOpenAIClients, resetOpenAIProviderState, isOpenAIAvailable, chatOpenAI, chatWithToolsOpenAI, } from './openai.js';
80
+ // Codex
33
81
  export { addResponsesApiProvider, isResponsesApiProvider, setCodexAuthPath, setCodexBaseUrl, initCodexAuth, resetCodexProviderState, loadCodexAuth, getCodexAuth, isCodexAvailable, chatCodex, chatWithToolsCodex, } from './codex.js';
34
- // Initialize all providers from config
82
+ // ---------------------------------------------------------------------------
83
+ // Provider Initialization (unchanged behavior)
84
+ // ---------------------------------------------------------------------------
35
85
  export async function initProviders(config) {
36
86
  // Reset provider state so reloads strictly reflect current config.
37
87
  setAnthropicClient(null);
38
88
  setUsingOAuth(false);
39
- clearOpenAIClients();
40
89
  resetCodexProviderState();
41
90
  const anthropicConfig = config.models.providers.anthropic;
42
91
  // Initialize Anthropic if configured
@@ -63,7 +112,7 @@ export async function initProviders(config) {
63
112
  console.log('[providers] Initialized anthropic');
64
113
  }
65
114
  }
66
- // Initialize all non-Anthropic providers
115
+ // Initialize Codex providers. API-key chat providers are not core.
67
116
  for (const [name, providerConfig] of Object.entries(config.models.providers)) {
68
117
  if (name === 'anthropic' || !providerConfig)
69
118
  continue;
@@ -82,63 +131,23 @@ export async function initProviders(config) {
82
131
  }
83
132
  continue;
84
133
  }
85
- const apiKey = providerConfig.apiKey;
86
- if (!apiKey)
87
- continue;
88
- const opts = { apiKey };
89
- if (providerConfig.baseURL) {
90
- let normalizedBaseURL = providerConfig.baseURL;
91
- if (name === 'minimax') {
92
- const trimmed = normalizedBaseURL.replace(/\/+$/, '');
93
- normalizedBaseURL = trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
94
- }
95
- opts.baseURL = normalizedBaseURL;
96
- }
97
- // Kimi Code API requires a coding-agent User-Agent with version string
98
- if (providerConfig.baseURL?.includes('kimi.com')) {
99
- opts.defaultHeaders = { 'User-Agent': 'claude-code/2.1.42' };
100
- }
101
- addOpenAIClient(name, new OpenAI(opts));
102
- console.log(`[providers] Initialized ${name}${providerConfig.baseURL ? ` (${providerConfig.baseURL})` : ''}`);
103
134
  }
104
135
  }
105
- // Unified chat function that routes to appropriate provider
136
+ // ---------------------------------------------------------------------------
137
+ // Unified chat + chatWithTools — route via adapter registry
138
+ // ---------------------------------------------------------------------------
139
+ /** Unified chat function that routes to appropriate provider via adapter. */
106
140
  export async function chat(messages, options, config) {
107
- const { resolvedModel, provider, chatOpts, useCodexAliasProvider } = normalizeChatRoute(options, config);
108
- // Route to Codex if available (supports openai/*-codex legacy alias)
109
- if ((isResponsesApiProvider(provider) || useCodexAliasProvider) && isCodexAvailable()) {
110
- return chatCodex({ messages, options: chatOpts, config });
111
- }
112
- if (isResponsesApiProvider(provider) || useCodexAliasProvider) {
113
- throw new Error(`Codex provider "${provider}" is configured but auth is unavailable. Run "codex" to re-authenticate.`);
114
- }
115
- // Route to Anthropic if available
116
- if (provider === 'anthropic' && isAnthropicAvailable()) {
117
- return chatAnthropic({ messages, options: chatOpts, config });
118
- }
119
- // Route to OpenAI-compatible
120
- if (isOpenAIAvailable(provider)) {
121
- return chatOpenAI({ messages, options: chatOpts, config }, provider);
122
- }
123
- throw new Error(`Unknown provider "${provider}" for model: ${resolvedModel}`);
141
+ const { adapter, chatOpts } = resolveAdapter(options, config);
142
+ return adapter.chat(messages, chatOpts, config);
124
143
  }
125
- // Unified chatWithTools function that routes to appropriate provider
144
+ /** Unified chatWithTools function that routes to appropriate provider via adapter. */
126
145
  export async function chatWithTools(messages, options, config, toolConfig, toolContext) {
127
- const { resolvedModel, provider, chatOpts, useCodexAliasProvider } = normalizeChatRoute(options, config);
128
- // Route to Codex if available (supports openai/*-codex legacy alias)
129
- if ((isResponsesApiProvider(provider) || useCodexAliasProvider) && isCodexAvailable()) {
130
- return chatWithToolsCodex({ messages, options: chatOpts, config, toolConfig, toolContext });
131
- }
132
- if (isResponsesApiProvider(provider) || useCodexAliasProvider) {
133
- throw new Error(`Codex provider "${provider}" is configured but auth is unavailable. Run "codex" to re-authenticate.`);
134
- }
135
- // Route to Anthropic if available
136
- if (provider === 'anthropic' && isAnthropicAvailable()) {
137
- return chatWithToolsAnthropic({ messages, options: chatOpts, config, toolConfig, toolContext });
138
- }
139
- // Route to OpenAI-compatible
140
- if (isOpenAIAvailable(provider)) {
141
- return chatWithToolsOpenAI({ messages, options: chatOpts, config, toolConfig, toolContext }, provider);
142
- }
143
- throw new Error(`Unknown provider "${provider}" for model: ${resolvedModel}`);
146
+ const { adapter, resolvedModel, chatOpts } = resolveAdapter(options, config);
147
+ // Codex default: bump maxIterations to 100 if not specified
148
+ const effectiveToolConfig = (adapter.name === 'codex' && !toolConfig.maxIterations)
149
+ ? { ...toolConfig, maxIterations: 100 }
150
+ : toolConfig;
151
+ const { runToolLoop } = await import('./tool-loop.js');
152
+ return runToolLoop(adapter, messages, chatOpts, config, effectiveToolConfig, toolContext);
144
153
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared utility functions for the tool loop.
3
+ * Extracted to eliminate duplication across provider implementations.
4
+ */
5
+ /**
6
+ * Build a standardized tool log entry.
7
+ */
8
+ export declare function buildToolLogEntry(toolName: string, inputStr: string, resultPreview: string): string;
9
+ /**
10
+ * Log iteration progress to console.
11
+ */
12
+ export declare function logIteration(provider: string, iteration: number, maxIterations: number, modelId: string): void;
13
+ /**
14
+ * Log compaction event.
15
+ */
16
+ export declare function logCompaction(provider: string, method: string, iteration: number): void;
17
+ /**
18
+ * Log max iterations warning.
19
+ */
20
+ export declare function logMaxIterations(provider: string, maxIterations: number): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared utility functions for the tool loop.
3
+ * Extracted to eliminate duplication across provider implementations.
4
+ */
5
+ /**
6
+ * Build a standardized tool log entry.
7
+ */
8
+ export function buildToolLogEntry(toolName, inputStr, resultPreview) {
9
+ const truncatedInput = inputStr.length > 100 ? inputStr.slice(0, 100) + '...' : inputStr;
10
+ const truncatedResult = resultPreview.length > 200 ? resultPreview.slice(0, 200) + '...' : resultPreview;
11
+ return `${toolName}(${truncatedInput}) → ${truncatedResult}`;
12
+ }
13
+ /**
14
+ * Log iteration progress to console.
15
+ */
16
+ export function logIteration(provider, iteration, maxIterations, modelId) {
17
+ console.log(`[${provider}] Iteration ${iteration + 1}/${maxIterations} (model: ${modelId})`);
18
+ }
19
+ /**
20
+ * Log compaction event.
21
+ */
22
+ export function logCompaction(provider, method, iteration) {
23
+ console.log(`[${provider}] Compacted messages (${method}) at iteration ${iteration + 1}`);
24
+ }
25
+ /**
26
+ * Log max iterations warning.
27
+ */
28
+ export function logMaxIterations(provider, maxIterations) {
29
+ console.warn(`[${provider}] Max iterations (${maxIterations}) reached without final answer`);
30
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Unified agentic tool loop - works with any provider adapter.
3
+ * Handles iteration control, tool execution, guard logic, compaction, and observability.
4
+ */
5
+ import type { ChatMessage, ChatOptions, Config, ToolConfig } from '../types.js';
6
+ import type { ToolChatResult } from './types.js';
7
+ import type { ExecuteToolContext } from '../tools.js';
8
+ import type { ProviderAdapter } from './adapter.js';
9
+ /**
10
+ * Run the unified agentic tool loop with any provider adapter.
11
+ */
12
+ export declare function runToolLoop(adapter: ProviderAdapter, messages: ChatMessage[], options: ChatOptions, config: Config, toolConfig: ToolConfig, toolContext?: ExecuteToolContext): Promise<ToolChatResult>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Unified agentic tool loop - works with any provider adapter.
3
+ * Handles iteration control, tool execution, guard logic, compaction, and observability.
4
+ */
5
+ import { getToolDefinitions, executeTool } from '../tools.js';
6
+ import { ToolCallGuard } from './tool-guard.js';
7
+ import { splitToolResult } from './utils.js';
8
+ import { startTrace, addEvent, endTrace } from '../audit.js';
9
+ import { toErrorMessage } from '../utils.js';
10
+ import { buildToolLogEntry, logIteration, logCompaction, logMaxIterations } from './loop-utils.js';
11
+ /** Start a Langfuse observation (lazy import to avoid circular deps). Returns null if disabled. */
12
+ async function tryStartObservation(name, params, type) {
13
+ try {
14
+ const { isLangfuseEnabled } = await import('../langfuse.js');
15
+ if (!isLangfuseEnabled())
16
+ return null;
17
+ const { startObservation } = await import('@langfuse/tracing');
18
+ return startObservation(name, params, { asType: type });
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /**
25
+ * Run the unified agentic tool loop with any provider adapter.
26
+ */
27
+ export async function runToolLoop(adapter, messages, options, config, toolConfig, toolContext) {
28
+ const maxIterations = toolConfig.maxIterations || 20;
29
+ const guard = new ToolCallGuard(toolConfig.maxTurnTokens);
30
+ const toolLog = [];
31
+ // Resolve tool definitions once
32
+ const includeSpawn = !!(toolContext?.fullConfig && (toolContext?.chatId || toolContext?.isCronJob));
33
+ const providerToolDefOptions = adapter.getToolDefinitionOptions?.(toolContext, config) || {};
34
+ const rawToolDefs = await getToolDefinitions(toolConfig, {
35
+ includeAgentTools: includeSpawn,
36
+ includeMcp: providerToolDefOptions.includeMcp,
37
+ projects: toolContext?.fullConfig?.projects,
38
+ });
39
+ // Build provider-specific tool definitions
40
+ const providerToolDefs = adapter.buildToolDefs(rawToolDefs, config);
41
+ // Build initial provider messages
42
+ const providerMessages = adapter.buildMessages(messages, options, config);
43
+ // Start audit trace if not already started
44
+ const trigger = (toolContext?.trigger || 'api');
45
+ const ownTrace = !toolContext?.auditTraceId;
46
+ const auditTraceId = toolContext?.auditTraceId || startTrace(trigger);
47
+ // Cumulative usage and cost across all iterations
48
+ let totalInputTokens = 0;
49
+ let totalOutputTokens = 0;
50
+ const totalCost = { input: 0, output: 0, total: 0 };
51
+ let traceStatus = 'ok';
52
+ try {
53
+ // Agentic loop
54
+ for (let i = 0; i < maxIterations; i++) {
55
+ // Check abort signal before each iteration
56
+ if (toolContext?.abortSignal?.aborted) {
57
+ return {
58
+ response: `[Cancelled after ${toolLog.length} tool calls]`,
59
+ toolCalls: toolLog,
60
+ };
61
+ }
62
+ // Compact messages if needed
63
+ const compactionResult = await adapter.compactMessages(providerMessages, toolConfig.contextManagement, i + 1, config);
64
+ if (compactionResult.compacted) {
65
+ logCompaction(adapter.name, compactionResult.method || 'unknown', i);
66
+ toolLog.push(`[context compacted via ${compactionResult.method}]`);
67
+ }
68
+ // Make API call
69
+ logIteration(adapter.name, i, maxIterations, options.model);
70
+ const genObs = await tryStartObservation(`${adapter.name}:${options.model}`, {
71
+ input: { messages: providerMessages.messages },
72
+ model: options.model,
73
+ modelParameters: { max_tokens: options.maxTokens },
74
+ metadata: { provider: adapter.name, iteration: i + 1 },
75
+ }, 'generation');
76
+ let response;
77
+ try {
78
+ response = await adapter.call(providerMessages, providerToolDefs, options, config);
79
+ // Record usage
80
+ adapter.recordUsage(options.model, response.usage, toolContext?.trigger || 'api', toolContext?.agentId);
81
+ // Accumulate usage across iterations
82
+ totalInputTokens += response.usage?.inputTokens ?? 0;
83
+ totalOutputTokens += response.usage?.outputTokens ?? 0;
84
+ // Accumulate cost
85
+ if (response.cost) {
86
+ totalCost.input += response.cost.input;
87
+ totalCost.output += response.cost.output;
88
+ totalCost.total += response.cost.total;
89
+ }
90
+ // Track tokens in guard (for stats only, no enforcement)
91
+ guard.recordTokens(response.usage?.inputTokens ?? 0, response.usage?.outputTokens ?? 0);
92
+ genObs?.update({ output: response.textContent });
93
+ genObs?.end();
94
+ }
95
+ catch (err) {
96
+ const errorMessage = toErrorMessage(err);
97
+ genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
98
+ genObs?.end();
99
+ traceStatus = 'error';
100
+ throw err;
101
+ }
102
+ // If no tool calls, we're done
103
+ if (!response.hasToolCalls) {
104
+ let responseText = response.textContent;
105
+ // Fallback when model returned no text
106
+ if (!responseText) {
107
+ console.warn(`[${adapter.name}] empty text response (stop_reason: ${response.rawResponse?.stop_reason}, content blocks: ${JSON.stringify((response.rawResponse?.content || []).map((b) => b.type))})`);
108
+ if (toolLog.length > 0) {
109
+ // Let adapter attempt a finalization pass (e.g. Codex re-asks without tools)
110
+ if (adapter.onEmptyFinalResponse) {
111
+ try {
112
+ const finalized = await adapter.onEmptyFinalResponse(providerMessages, providerToolDefs, options, config);
113
+ if (finalized)
114
+ responseText = finalized;
115
+ }
116
+ catch (err) {
117
+ console.warn(`[${adapter.name}] finalization pass failed: ${toErrorMessage(err)}`);
118
+ }
119
+ }
120
+ if (!responseText) {
121
+ responseText = `[Completed with ${toolLog.length} tool calls, no text response]`;
122
+ }
123
+ }
124
+ else {
125
+ responseText = '[Model returned empty response — please try again]';
126
+ }
127
+ }
128
+ return {
129
+ response: responseText,
130
+ toolCalls: toolLog,
131
+ usage: {
132
+ prompt_tokens: totalInputTokens,
133
+ completion_tokens: totalOutputTokens,
134
+ total_tokens: totalInputTokens + totalOutputTokens,
135
+ },
136
+ cost: totalCost.total > 0 ? totalCost : undefined,
137
+ };
138
+ }
139
+ // Append assistant's response to history
140
+ adapter.appendAssistantResponse(providerMessages, response.rawResponse);
141
+ // Execute each tool call, collecting results for batching
142
+ const toolResults = [];
143
+ for (const toolCall of response.toolCalls) {
144
+ const result = await executeToolCall(toolCall, guard, toolConfig, toolContext, toolLog, adapter.name);
145
+ toolResults.push(result);
146
+ }
147
+ // Append all tool results — batch if adapter supports it, otherwise one at a time
148
+ if (adapter.appendToolResults && toolResults.length > 1) {
149
+ adapter.appendToolResults(providerMessages, toolResults);
150
+ }
151
+ else {
152
+ for (const tr of toolResults) {
153
+ adapter.appendToolResult(providerMessages, tr.toolCallId, tr.result, tr.isError);
154
+ }
155
+ }
156
+ }
157
+ // Max iterations reached
158
+ logMaxIterations(adapter.name, maxIterations);
159
+ let responseText = `Tool use loop reached maximum iterations (${maxIterations}) before the model produced a final answer.`;
160
+ if (toolLog.length > 0 && adapter.onEmptyFinalResponse) {
161
+ try {
162
+ const finalized = await adapter.onEmptyFinalResponse(providerMessages, providerToolDefs, options, config);
163
+ if (finalized)
164
+ responseText = finalized;
165
+ }
166
+ catch (err) {
167
+ console.warn(`[${adapter.name}] max-iteration finalization pass failed: ${toErrorMessage(err)}`);
168
+ }
169
+ }
170
+ return {
171
+ response: responseText,
172
+ toolCalls: toolLog,
173
+ usage: {
174
+ prompt_tokens: totalInputTokens,
175
+ completion_tokens: totalOutputTokens,
176
+ total_tokens: totalInputTokens + totalOutputTokens,
177
+ },
178
+ cost: totalCost.total > 0 ? totalCost : undefined,
179
+ };
180
+ }
181
+ finally {
182
+ // End the audit trace if we created it
183
+ if (ownTrace) {
184
+ await endTrace(auditTraceId, traceStatus).catch(() => { });
185
+ }
186
+ }
187
+ }
188
+ /**
189
+ * Execute a single tool call and return the result (does NOT append to messages).
190
+ */
191
+ async function executeToolCall(toolCall, guard, toolConfig, toolContext, toolLog, providerName) {
192
+ const inputStr = toolCall.rawArgs.slice(0, 200);
193
+ console.log(`[${providerName}:tools] -> ${toolCall.name}(${inputStr})`);
194
+ // Guard: spin detection
195
+ const guardResult = guard.recordCall(toolCall.name, toolCall.args);
196
+ if (guardResult.warning) {
197
+ console.warn(`[${providerName}:tools:guard] ${guardResult.warning}`);
198
+ }
199
+ if (guardResult.blocked) {
200
+ toolLog.push(`${toolCall.name} [BLOCKED: spin detected]`);
201
+ return {
202
+ toolCallId: toolCall.id,
203
+ result: guardResult.warning || 'Blocked: repeated identical call',
204
+ isError: true,
205
+ };
206
+ }
207
+ // Execute tool
208
+ const toolObs = await tryStartObservation(`tool:${toolCall.name}`, { input: toolCall.args, metadata: { app: 'skimpyclaw', tool: toolCall.name } }, 'tool');
209
+ const toolStart = Date.now();
210
+ try {
211
+ const result = await executeTool(toolCall.name, toolCall.args, toolConfig, toolContext);
212
+ const truncatedResult = splitToolResult(toolCall.name, toolCall.args, result);
213
+ const resultPreview = result.slice(0, 200) + (result.length > 200 ? '...' : '');
214
+ console.log(`[${providerName}:tools] <- ${resultPreview}`);
215
+ toolLog.push(buildToolLogEntry(toolCall.name, inputStr, resultPreview));
216
+ toolObs?.update({ output: result });
217
+ toolObs?.end();
218
+ // Record audit event
219
+ if (toolContext?.auditTraceId) {
220
+ addEvent(toolContext.auditTraceId, {
221
+ type: 'tool_use',
222
+ summary: `${toolCall.name}(${inputStr})`,
223
+ durationMs: Date.now() - toolStart,
224
+ });
225
+ }
226
+ // Guard: no-progress detection
227
+ const progressResult = guard.recordResult(result);
228
+ let finalResult = truncatedResult;
229
+ if (progressResult.nudge) {
230
+ console.warn(`[${providerName}:tools:guard] ${progressResult.nudge}`);
231
+ finalResult += `\n\n[System: ${progressResult.nudge}]`;
232
+ }
233
+ return { toolCallId: toolCall.id, result: finalResult, isError: false };
234
+ }
235
+ catch (err) {
236
+ const errorMessage = toErrorMessage(err);
237
+ toolObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
238
+ toolObs?.end();
239
+ if (toolContext?.auditTraceId) {
240
+ addEvent(toolContext.auditTraceId, {
241
+ type: 'tool_error',
242
+ summary: `${toolCall.name} error: ${errorMessage.slice(0, 150)}`,
243
+ durationMs: Date.now() - toolStart,
244
+ });
245
+ }
246
+ const errorResult = `[Tool Error] ${toolCall.name}: ${errorMessage}`;
247
+ console.error(`[${providerName}:tools] tool error: ${errorMessage}`);
248
+ toolLog.push(`${toolCall.name} [ERROR: ${errorMessage.slice(0, 100)}]`);
249
+ return { toolCallId: toolCall.id, result: errorResult, isError: true };
250
+ }
251
+ }
@@ -1,5 +1,5 @@
1
- import type { ContentBlock, Config } from '../types.js';
2
- export declare const TOOL_GUARD = "You are a personal assistant running inside SkimpyClaw.\nYou are NOT the full Claude Code CLI. Do NOT roleplay as Claude Code.\n\n## Tool Rules\n- You have ONLY the tools provided via the API tool_use mechanism.\n- Tool names are case-sensitive. Call tools exactly as listed.\n- NEVER output tool calls as text/XML/JSON. Use the API tool_use mechanism only.\n- NEVER fabricate tool results or file contents. If you haven't read a file, say so.\n- NEVER invent tools that are not in your tool list (no str_replace_editor, no view, etc.)\n- If a Browser tool is available, you DO have web-browsing access via that tool. Use it instead of claiming you can't browse.\n- If you need information, use a tool to get it. Do not guess.";
1
+ import type { ContentBlock, Config, ThinkingLevel } from '../types.js';
2
+ export declare const TOOL_GUARD = "You are SkimpyClaw (NOT Claude Code CLI). Use only API tool_use \u2014 never text/XML tools. Never fabricate results.";
3
3
  export declare function setUsingOAuth(value: boolean): void;
4
4
  export declare function isUsingOAuth(): boolean;
5
5
  /**
@@ -60,10 +60,26 @@ export declare function stripProvider(model: string, openaiClients?: Map<string,
60
60
  * Falls back to simple truncation if file write fails.
61
61
  */
62
62
  export declare function truncateToolResult(result: string, _maxBytes?: number): string;
63
+ /**
64
+ * Structured split tool results: generates a semantic summary based on tool type.
65
+ * For small results (<= MASK_THRESHOLD), returns unchanged.
66
+ * For large results, writes full output to scratch file and returns a compact,
67
+ * tool-aware summary with the scratch file path.
68
+ */
69
+ export declare function splitToolResult(toolName: string, toolInput: Record<string, any>, result: string): string;
70
+ /**
71
+ * Compact old tool results in Anthropic-format messages.
72
+ * Replaces tool_result content with '✓' for all results except the last
73
+ * `keepRecent` messages. The model has already processed these results,
74
+ * so we only need to preserve the structure (tool_use_id matching).
75
+ *
76
+ * Mutates the messages array in place for efficiency.
77
+ */
78
+ export declare function compactOldResults(messages: any[], keepRecent?: number): void;
63
79
  /**
64
80
  * Build thinking config based on thinking level.
65
81
  */
66
- export declare function buildThinkingConfig(thinking?: 'none' | 'low' | 'medium' | 'high'): {
82
+ export declare function buildThinkingConfig(thinking?: ThinkingLevel): {
67
83
  budget: number;
68
84
  maxTokens: number;
69
85
  } | undefined;