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.
- package/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/providers/index.js
CHANGED
|
@@ -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
|
|
11
|
-
import { setAnthropicClient,
|
|
12
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
// Codex
|
|
33
81
|
export { addResponsesApiProvider, isResponsesApiProvider, setCodexAuthPath, setCodexBaseUrl, initCodexAuth, resetCodexProviderState, loadCodexAuth, getCodexAuth, isCodexAvailable, chatCodex, chatWithToolsCodex, } from './codex.js';
|
|
34
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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 {
|
|
108
|
-
|
|
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
|
-
|
|
144
|
+
/** Unified chatWithTools function that routes to appropriate provider via adapter. */
|
|
126
145
|
export async function chatWithTools(messages, options, config, toolConfig, toolContext) {
|
|
127
|
-
const {
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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?:
|
|
82
|
+
export declare function buildThinkingConfig(thinking?: ThinkingLevel): {
|
|
67
83
|
budget: number;
|
|
68
84
|
maxTokens: number;
|
|
69
85
|
} | undefined;
|