protoagent 0.1.2 → 0.1.3
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/dist/App.js +19 -7
- package/dist/agentic-loop.js +4 -2
- package/dist/components/ConfigDialog.js +6 -4
- package/dist/config.js +45 -17
- package/dist/mcp.js +20 -19
- package/dist/providers.js +73 -124
- package/dist/runtime-config.js +175 -0
- package/dist/sub-agent.js +2 -1
- package/dist/utils/compactor.js +9 -4
- package/package.json +2 -1
package/dist/App.js
CHANGED
|
@@ -12,7 +12,8 @@ import { TextInput, Select, PasswordInput } from '@inkjs/ui';
|
|
|
12
12
|
import BigText from 'ink-big-text';
|
|
13
13
|
import { OpenAI } from 'openai';
|
|
14
14
|
import { readConfig, writeConfig, resolveApiKey } from './config.js';
|
|
15
|
-
import {
|
|
15
|
+
import { loadRuntimeConfig } from './runtime-config.js';
|
|
16
|
+
import { getAllProviders, getProvider, getModelPricing, getRequestDefaultParams } from './providers.js';
|
|
16
17
|
import { runAgenticLoop, initializeMessages, } from './agentic-loop.js';
|
|
17
18
|
import { setDangerouslyAcceptAll, setApprovalHandler, clearApprovalHandler } from './tools/index.js';
|
|
18
19
|
import { setLogLevel, LogLevel, initLogFile, logger } from './utils/logger.js';
|
|
@@ -143,7 +144,7 @@ function buildClient(config) {
|
|
|
143
144
|
if (baseURL) {
|
|
144
145
|
clientOptions.baseURL = baseURL;
|
|
145
146
|
}
|
|
146
|
-
// Custom headers:
|
|
147
|
+
// Custom headers: env override takes precedence over provider defaults
|
|
147
148
|
const rawHeaders = process.env.PROTOAGENT_CUSTOM_HEADERS?.trim();
|
|
148
149
|
if (rawHeaders) {
|
|
149
150
|
const defaultHeaders = {};
|
|
@@ -160,6 +161,9 @@ function buildClient(config) {
|
|
|
160
161
|
clientOptions.defaultHeaders = defaultHeaders;
|
|
161
162
|
}
|
|
162
163
|
}
|
|
164
|
+
else if (provider?.headers && Object.keys(provider.headers).length > 0) {
|
|
165
|
+
clientOptions.defaultHeaders = provider.headers;
|
|
166
|
+
}
|
|
163
167
|
return new OpenAI(clientOptions);
|
|
164
168
|
}
|
|
165
169
|
// ─── Sub-components ───
|
|
@@ -196,7 +200,7 @@ const InlineSetup = ({ onComplete }) => {
|
|
|
196
200
|
const [selectedProviderId, setSelectedProviderId] = useState('');
|
|
197
201
|
const [selectedModelId, setSelectedModelId] = useState('');
|
|
198
202
|
const [apiKeyError, setApiKeyError] = useState('');
|
|
199
|
-
const providerItems =
|
|
203
|
+
const providerItems = getAllProviders().flatMap((provider) => provider.models.map((model) => ({
|
|
200
204
|
label: `${provider.name} - ${model.name}`,
|
|
201
205
|
value: `${provider.id}:::${model.id}`,
|
|
202
206
|
})));
|
|
@@ -209,15 +213,16 @@ const InlineSetup = ({ onComplete }) => {
|
|
|
209
213
|
} }) })] }));
|
|
210
214
|
}
|
|
211
215
|
const provider = getProvider(selectedProviderId);
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
const hasResolvedAuth = Boolean(resolveApiKey({ provider: selectedProviderId, apiKey: undefined }));
|
|
217
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "First-time setup" }), _jsxs(Text, { dimColor: true, children: ["Selected: ", provider?.name, " / ", selectedModelId] }), _jsx(Text, { children: hasResolvedAuth ? 'Optional API key:' : 'Enter your API key:' }), apiKeyError && _jsx(Text, { color: "red", children: apiKeyError }), _jsx(PasswordInput, { placeholder: hasResolvedAuth ? 'Press enter to keep resolved auth' : `Paste your ${provider?.apiKeyEnvVar || 'API'} key`, onSubmit: (value) => {
|
|
218
|
+
if (value.trim().length === 0 && !hasResolvedAuth) {
|
|
214
219
|
setApiKeyError('API key cannot be empty.');
|
|
215
220
|
return;
|
|
216
221
|
}
|
|
217
222
|
const newConfig = {
|
|
218
223
|
provider: selectedProviderId,
|
|
219
224
|
model: selectedModelId,
|
|
220
|
-
apiKey: value.trim(),
|
|
225
|
+
...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
|
|
221
226
|
};
|
|
222
227
|
writeConfig(newConfig);
|
|
223
228
|
onComplete(newConfig);
|
|
@@ -331,6 +336,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
331
336
|
setPendingApproval({ request: req, resolve });
|
|
332
337
|
});
|
|
333
338
|
});
|
|
339
|
+
await loadRuntimeConfig();
|
|
334
340
|
// Load config — if none exists, show inline setup
|
|
335
341
|
const loadedConfig = readConfig();
|
|
336
342
|
if (!loadedConfig) {
|
|
@@ -436,6 +442,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
436
442
|
assistantMessageRef.current = null;
|
|
437
443
|
try {
|
|
438
444
|
const pricing = getModelPricing(config.provider, config.model);
|
|
445
|
+
const requestDefaults = getRequestDefaultParams(config.provider, config.model);
|
|
439
446
|
// Create abort controller for this completion
|
|
440
447
|
abortControllerRef.current = new AbortController();
|
|
441
448
|
const updatedMessages = await runAgenticLoop(clientRef.current, config.model, [...completionMessages, userMessage], trimmed, (event) => {
|
|
@@ -568,7 +575,12 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
568
575
|
setThreadErrors((prev) => prev.filter((threadError) => !threadError.transient));
|
|
569
576
|
break;
|
|
570
577
|
}
|
|
571
|
-
}, {
|
|
578
|
+
}, {
|
|
579
|
+
pricing: pricing || undefined,
|
|
580
|
+
abortSignal: abortControllerRef.current.signal,
|
|
581
|
+
sessionId: session?.id,
|
|
582
|
+
requestDefaults,
|
|
583
|
+
});
|
|
572
584
|
// Final update to ensure we have the complete message history
|
|
573
585
|
setCompletionMessages(updatedMessages);
|
|
574
586
|
// Update session
|
package/dist/agentic-loop.js
CHANGED
|
@@ -219,6 +219,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
219
219
|
const pricing = options.pricing;
|
|
220
220
|
const abortSignal = options.abortSignal;
|
|
221
221
|
const sessionId = options.sessionId;
|
|
222
|
+
const requestDefaults = options.requestDefaults || {};
|
|
222
223
|
// Note: userInput is passed for context/logging but user message should already be in messages array
|
|
223
224
|
// (added by the caller in handleSubmit for immediate UI display)
|
|
224
225
|
const updatedMessages = [...messages];
|
|
@@ -243,7 +244,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
243
244
|
if (pricing) {
|
|
244
245
|
const contextInfo = getContextInfo(updatedMessages, pricing);
|
|
245
246
|
if (contextInfo.needsCompaction) {
|
|
246
|
-
const compacted = await compactIfNeeded(client, model, updatedMessages, pricing.contextWindow, contextInfo.currentTokens);
|
|
247
|
+
const compacted = await compactIfNeeded(client, model, updatedMessages, pricing.contextWindow, contextInfo.currentTokens, requestDefaults, sessionId);
|
|
247
248
|
// Replace messages in-place
|
|
248
249
|
updatedMessages.length = 0;
|
|
249
250
|
updatedMessages.push(...compacted);
|
|
@@ -287,6 +288,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
290
|
const stream = await client.chat.completions.create({
|
|
291
|
+
...requestDefaults,
|
|
290
292
|
model,
|
|
291
293
|
messages: updatedMessages,
|
|
292
294
|
tools: allTools,
|
|
@@ -399,7 +401,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
399
401
|
let result;
|
|
400
402
|
// Handle sub-agent tool specially
|
|
401
403
|
if (name === 'sub_agent') {
|
|
402
|
-
result = await runSubAgent(client, model, args.task, args.max_iterations);
|
|
404
|
+
result = await runSubAgent(client, model, args.task, args.max_iterations, requestDefaults);
|
|
403
405
|
}
|
|
404
406
|
else {
|
|
405
407
|
result = await handleToolCall(name, args, { sessionId });
|
|
@@ -7,12 +7,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
import { useState } from 'react';
|
|
8
8
|
import { Box, Text } from 'ink';
|
|
9
9
|
import { PasswordInput, Select } from '@inkjs/ui';
|
|
10
|
-
import {
|
|
10
|
+
import { getAllProviders, getProvider } from '../providers.js';
|
|
11
|
+
import { resolveApiKey } from '../config.js';
|
|
11
12
|
export const ConfigDialog = ({ currentConfig, onComplete, onCancel, }) => {
|
|
12
13
|
const [step, setStep] = useState('select_provider');
|
|
13
14
|
const [selectedProviderId, setSelectedProviderId] = useState(currentConfig.provider);
|
|
14
15
|
const [selectedModelId, setSelectedModelId] = useState(currentConfig.model);
|
|
15
|
-
const providerItems =
|
|
16
|
+
const providerItems = getAllProviders().flatMap((provider) => provider.models.map((model) => ({
|
|
16
17
|
label: `${provider.name} - ${model.name}`,
|
|
17
18
|
value: `${provider.id}:::${model.id}`,
|
|
18
19
|
})));
|
|
@@ -28,12 +29,13 @@ export const ConfigDialog = ({ currentConfig, onComplete, onCancel, }) => {
|
|
|
28
29
|
}
|
|
29
30
|
// API key entry step
|
|
30
31
|
const provider = getProvider(selectedProviderId);
|
|
31
|
-
|
|
32
|
+
const hasResolvedAuth = Boolean(resolveApiKey({ provider: selectedProviderId, apiKey: undefined }));
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Confirm Configuration" }), _jsxs(Text, { dimColor: true, children: ["Provider: ", provider?.name, " / ", selectedModelId] }), _jsx(Text, { children: hasResolvedAuth ? 'Optional API key (leave empty to keep resolved auth):' : 'Enter your API key:' }), _jsx(PasswordInput, { placeholder: `Paste your ${provider?.apiKeyEnvVar || 'API'} key`, onSubmit: (value) => {
|
|
32
34
|
const finalApiKey = value.trim().length > 0 ? value.trim() : currentConfig.apiKey;
|
|
33
35
|
const newConfig = {
|
|
34
36
|
provider: selectedProviderId,
|
|
35
37
|
model: selectedModelId,
|
|
36
|
-
apiKey: finalApiKey,
|
|
38
|
+
...(finalApiKey?.trim() ? { apiKey: finalApiKey.trim() } : {}),
|
|
37
39
|
};
|
|
38
40
|
onComplete(newConfig);
|
|
39
41
|
} })] }));
|
package/dist/config.js
CHANGED
|
@@ -5,7 +5,8 @@ import os from 'node:os';
|
|
|
5
5
|
import { useState, useEffect } from 'react';
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
7
|
import { Select, TextInput, PasswordInput } from '@inkjs/ui';
|
|
8
|
-
import {
|
|
8
|
+
import { loadRuntimeConfig } from './runtime-config.js';
|
|
9
|
+
import { getAllProviders, getProvider } from './providers.js';
|
|
9
10
|
const CONFIG_DIR_MODE = 0o700;
|
|
10
11
|
const CONFIG_FILE_MODE = 0o600;
|
|
11
12
|
function hardenPermissions(targetPath, mode) {
|
|
@@ -18,16 +19,35 @@ export function resolveApiKey(config) {
|
|
|
18
19
|
if (directApiKey) {
|
|
19
20
|
return directApiKey;
|
|
20
21
|
}
|
|
22
|
+
const provider = getProvider(config.provider);
|
|
23
|
+
if (provider?.apiKeyEnvVar) {
|
|
24
|
+
const providerEnvOverride = process.env[provider.apiKeyEnvVar]?.trim();
|
|
25
|
+
if (providerEnvOverride) {
|
|
26
|
+
return providerEnvOverride;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const envOverride = process.env.PROTOAGENT_API_KEY?.trim();
|
|
30
|
+
if (envOverride) {
|
|
31
|
+
return envOverride;
|
|
32
|
+
}
|
|
33
|
+
const providerApiKey = provider?.apiKey?.trim();
|
|
34
|
+
if (providerApiKey) {
|
|
35
|
+
return providerApiKey;
|
|
36
|
+
}
|
|
21
37
|
// Fallback for Cloudflare Gateway or other custom header setups
|
|
22
38
|
if (process.env.PROTOAGENT_CUSTOM_HEADERS) {
|
|
23
39
|
return 'none';
|
|
24
40
|
}
|
|
25
|
-
const provider = getProvider(config.provider);
|
|
26
41
|
if (!provider?.apiKeyEnvVar) {
|
|
42
|
+
if (provider?.headers && Object.keys(provider.headers).length > 0) {
|
|
43
|
+
return 'none';
|
|
44
|
+
}
|
|
27
45
|
return null;
|
|
28
46
|
}
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
if (provider?.headers && Object.keys(provider.headers).length > 0) {
|
|
48
|
+
return 'none';
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
31
51
|
}
|
|
32
52
|
export const getConfigDirectory = () => {
|
|
33
53
|
const homeDir = os.homedir();
|
|
@@ -55,8 +75,8 @@ export const readConfig = () => {
|
|
|
55
75
|
// Handle legacy format: { provider, model, credentials: { KEY: "..." } }
|
|
56
76
|
let apiKey = raw.apiKey;
|
|
57
77
|
if (!apiKey && raw.credentials && typeof raw.credentials === 'object') {
|
|
58
|
-
const provider =
|
|
59
|
-
if (provider) {
|
|
78
|
+
const provider = getAllProviders().find((p) => p.id === raw.provider);
|
|
79
|
+
if (provider?.apiKeyEnvVar) {
|
|
60
80
|
apiKey = raw.credentials[provider.apiKeyEnvVar];
|
|
61
81
|
}
|
|
62
82
|
// Fallback: grab the first non-empty value
|
|
@@ -93,14 +113,21 @@ export const writeConfig = (config) => {
|
|
|
93
113
|
};
|
|
94
114
|
export const InitialLoading = ({ setExistingConfig, setStep }) => {
|
|
95
115
|
useEffect(() => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
loadRuntimeConfig()
|
|
117
|
+
.then(() => {
|
|
118
|
+
const config = readConfig();
|
|
119
|
+
if (config) {
|
|
120
|
+
setExistingConfig(config);
|
|
121
|
+
setStep(1);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
setStep(2);
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.catch((error) => {
|
|
128
|
+
console.error('Error loading runtime config:', error);
|
|
102
129
|
setStep(2);
|
|
103
|
-
}
|
|
130
|
+
});
|
|
104
131
|
}, []);
|
|
105
132
|
return _jsx(Text, { children: "Loading configuration..." });
|
|
106
133
|
};
|
|
@@ -118,7 +145,7 @@ export const ResetPrompt = ({ existingConfig, setStep, setConfigWritten }) => {
|
|
|
118
145
|
} })] }));
|
|
119
146
|
};
|
|
120
147
|
export const ModelSelection = ({ setSelectedProviderId, setSelectedModelId, setStep, }) => {
|
|
121
|
-
const items =
|
|
148
|
+
const items = getAllProviders().flatMap((provider) => provider.models.map((model) => ({
|
|
122
149
|
label: `${provider.name} - ${model.name}`,
|
|
123
150
|
value: `${provider.id}:::${model.id}`,
|
|
124
151
|
})));
|
|
@@ -133,21 +160,22 @@ export const ModelSelection = ({ setSelectedProviderId, setSelectedModelId, setS
|
|
|
133
160
|
export const ApiKeyInput = ({ selectedProviderId, selectedModelId, setStep, setConfigWritten, }) => {
|
|
134
161
|
const [errorMessage, setErrorMessage] = useState('');
|
|
135
162
|
const provider = getProvider(selectedProviderId);
|
|
163
|
+
const canUseResolvedAuth = Boolean(resolveApiKey({ provider: selectedProviderId, apiKey: undefined }));
|
|
136
164
|
const handleApiKeySubmit = (value) => {
|
|
137
|
-
if (value.trim().length === 0) {
|
|
165
|
+
if (value.trim().length === 0 && !canUseResolvedAuth) {
|
|
138
166
|
setErrorMessage('API key cannot be empty.');
|
|
139
167
|
return;
|
|
140
168
|
}
|
|
141
169
|
const newConfig = {
|
|
142
170
|
provider: selectedProviderId,
|
|
143
171
|
model: selectedModelId,
|
|
144
|
-
apiKey: value.trim(),
|
|
172
|
+
...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
|
|
145
173
|
};
|
|
146
174
|
writeConfig(newConfig);
|
|
147
175
|
setConfigWritten(true);
|
|
148
176
|
setStep(4);
|
|
149
177
|
};
|
|
150
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [
|
|
178
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [canUseResolvedAuth ? 'Optional API Key' : 'Enter API Key', " for ", provider?.name || selectedProviderId, ":"] }), provider?.headers && Object.keys(provider.headers).length > 0 && (_jsx(Text, { dimColor: true, children: "This provider can authenticate with configured headers or environment variables." })), errorMessage && _jsx(Text, { color: "red", children: errorMessage }), _jsx(PasswordInput, { placeholder: canUseResolvedAuth ? 'Press enter to keep resolved auth' : `Enter your ${provider?.apiKeyEnvVar || 'API'} key`, onSubmit: handleApiKeySubmit })] }));
|
|
151
179
|
};
|
|
152
180
|
export const ConfigResult = ({ configWritten }) => {
|
|
153
181
|
return (_jsxs(Box, { flexDirection: "column", children: [configWritten ? (_jsx(Text, { color: "green", children: "Configuration saved successfully!" })) : (_jsx(Text, { color: "yellow", children: "Configuration not changed." })), _jsx(Text, { children: "You can now run ProtoAgent." })] }));
|
package/dist/mcp.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses the official @modelcontextprotocol/sdk to connect to MCP servers
|
|
5
5
|
* over both stdio (spawned processes) and HTTP transports.
|
|
6
6
|
*
|
|
7
|
-
* Configuration in
|
|
7
|
+
* Configuration in `protoagent.jsonc` under `mcp.servers`:
|
|
8
8
|
* {
|
|
9
9
|
* "servers": {
|
|
10
10
|
* "my-stdio-server": {
|
|
@@ -23,11 +23,10 @@
|
|
|
23
23
|
* Stdio servers are spawned as child processes communicating over stdin/stdout.
|
|
24
24
|
* HTTP servers connect to a running server via HTTP POST/GET with SSE streaming.
|
|
25
25
|
*/
|
|
26
|
-
import fs from 'node:fs/promises';
|
|
27
|
-
import path from 'node:path';
|
|
28
26
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
29
27
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
30
28
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
29
|
+
import { loadRuntimeConfig, getRuntimeConfig } from './runtime-config.js';
|
|
31
30
|
import { logger } from './utils/logger.js';
|
|
32
31
|
import { registerDynamicTool, registerDynamicHandler } from './tools/index.js';
|
|
33
32
|
const connections = new Map();
|
|
@@ -38,7 +37,11 @@ async function connectStdioServer(serverName, config) {
|
|
|
38
37
|
const transport = new StdioClientTransport({
|
|
39
38
|
command: config.command,
|
|
40
39
|
args: config.args || [],
|
|
41
|
-
env:
|
|
40
|
+
env: {
|
|
41
|
+
...process.env,
|
|
42
|
+
...(config.env || {}),
|
|
43
|
+
},
|
|
44
|
+
cwd: config.cwd,
|
|
42
45
|
});
|
|
43
46
|
const client = new Client({
|
|
44
47
|
name: 'protoagent',
|
|
@@ -57,7 +60,9 @@ async function connectStdioServer(serverName, config) {
|
|
|
57
60
|
* Create an MCP client connection for an HTTP server.
|
|
58
61
|
*/
|
|
59
62
|
async function connectHttpServer(serverName, config) {
|
|
60
|
-
const transport = new StreamableHTTPClientTransport(new URL(config.url)
|
|
63
|
+
const transport = new StreamableHTTPClientTransport(new URL(config.url), {
|
|
64
|
+
requestInit: config.headers ? { headers: config.headers } : undefined,
|
|
65
|
+
});
|
|
61
66
|
const client = new Client({
|
|
62
67
|
name: 'protoagent',
|
|
63
68
|
version: '0.0.1',
|
|
@@ -92,7 +97,7 @@ async function registerMcpTools(conn) {
|
|
|
92
97
|
registerDynamicHandler(toolName, async (args) => {
|
|
93
98
|
const result = await conn.client.callTool({
|
|
94
99
|
name: tool.name,
|
|
95
|
-
arguments: args,
|
|
100
|
+
arguments: (args && typeof args === 'object' ? args : {}),
|
|
96
101
|
});
|
|
97
102
|
// MCP tool results are arrays of content blocks
|
|
98
103
|
if (Array.isArray(result.content)) {
|
|
@@ -117,20 +122,16 @@ async function registerMcpTools(conn) {
|
|
|
117
122
|
* Registers their tools in the dynamic tool registry.
|
|
118
123
|
*/
|
|
119
124
|
export async function initializeMcp() {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const content = await fs.readFile(configPath, 'utf8');
|
|
124
|
-
config = JSON.parse(content);
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// No MCP config — that's fine, most projects won't have one
|
|
125
|
+
await loadRuntimeConfig();
|
|
126
|
+
const servers = getRuntimeConfig().mcp?.servers || {};
|
|
127
|
+
if (Object.keys(servers).length === 0)
|
|
128
128
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
logger.info('Loading MCP servers from merged runtime config');
|
|
130
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
131
|
+
if (serverConfig.enabled === false) {
|
|
132
|
+
logger.debug(`Skipping disabled MCP server: ${name}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
134
135
|
try {
|
|
135
136
|
let conn;
|
|
136
137
|
if (serverConfig.type === 'stdio') {
|
package/dist/providers.js
CHANGED
|
@@ -1,37 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provider and model registry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and API key env var name, and any models it supports.
|
|
4
|
+
* Built-in providers are declared in source and merged with runtime overrides
|
|
5
|
+
* from `protoagent.jsonc`.
|
|
7
6
|
*/
|
|
8
|
-
|
|
7
|
+
import { getRuntimeConfig } from './runtime-config.js';
|
|
8
|
+
export const BUILTIN_PROVIDERS = [
|
|
9
9
|
{
|
|
10
10
|
id: 'openai',
|
|
11
11
|
name: 'OpenAI',
|
|
12
12
|
apiKeyEnvVar: 'OPENAI_API_KEY',
|
|
13
13
|
models: [
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
contextWindow: 200_000,
|
|
18
|
-
pricingPerMillionInput: 6.00,
|
|
19
|
-
pricingPerMillionOutput: 24.00,
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
id: 'gpt-5-mini',
|
|
23
|
-
name: 'GPT-5 Mini',
|
|
24
|
-
contextWindow: 200_000,
|
|
25
|
-
pricingPerMillionInput: 0.15,
|
|
26
|
-
pricingPerMillionOutput: 0.60,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
id: 'gpt-4.1',
|
|
30
|
-
name: 'GPT-4.1',
|
|
31
|
-
contextWindow: 128_000,
|
|
32
|
-
pricingPerMillionInput: 2.50,
|
|
33
|
-
pricingPerMillionOutput: 10.00,
|
|
34
|
-
},
|
|
14
|
+
{ id: 'gpt-5.2', name: 'GPT-5.2', contextWindow: 200_000, pricingPerMillionInput: 6.0, pricingPerMillionOutput: 24.0 },
|
|
15
|
+
{ id: 'gpt-5-mini', name: 'GPT-5 Mini', contextWindow: 200_000, pricingPerMillionInput: 0.15, pricingPerMillionOutput: 0.6 },
|
|
16
|
+
{ id: 'gpt-4.1', name: 'GPT-4.1', contextWindow: 128_000, pricingPerMillionInput: 2.5, pricingPerMillionOutput: 10.0 },
|
|
35
17
|
],
|
|
36
18
|
},
|
|
37
19
|
{
|
|
@@ -40,27 +22,9 @@ export const SUPPORTED_MODELS = [
|
|
|
40
22
|
baseURL: 'https://api.anthropic.com/v1/',
|
|
41
23
|
apiKeyEnvVar: 'ANTHROPIC_API_KEY',
|
|
42
24
|
models: [
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
contextWindow: 200_000,
|
|
47
|
-
pricingPerMillionInput: 5.00,
|
|
48
|
-
pricingPerMillionOutput: 25.00,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'claude-sonnet-4-6',
|
|
52
|
-
name: 'Claude Sonnet 4.6',
|
|
53
|
-
contextWindow: 200_000,
|
|
54
|
-
pricingPerMillionInput: 3.00,
|
|
55
|
-
pricingPerMillionOutput: 15.00,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
id: 'claude-haiku-4-5',
|
|
59
|
-
name: 'Claude Haiku 4.5',
|
|
60
|
-
contextWindow: 200_000,
|
|
61
|
-
pricingPerMillionInput: 1.00,
|
|
62
|
-
pricingPerMillionOutput: 5.00,
|
|
63
|
-
},
|
|
25
|
+
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', contextWindow: 200_000, pricingPerMillionInput: 5.0, pricingPerMillionOutput: 25.0 },
|
|
26
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', contextWindow: 200_000, pricingPerMillionInput: 3.0, pricingPerMillionOutput: 15.0 },
|
|
27
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', contextWindow: 200_000, pricingPerMillionInput: 1.0, pricingPerMillionOutput: 5.0 },
|
|
64
28
|
],
|
|
65
29
|
},
|
|
66
30
|
{
|
|
@@ -69,34 +33,10 @@ export const SUPPORTED_MODELS = [
|
|
|
69
33
|
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
70
34
|
apiKeyEnvVar: 'GEMINI_API_KEY',
|
|
71
35
|
models: [
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
pricingPerMillionInput: 0.075,
|
|
77
|
-
pricingPerMillionOutput: 0.30,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: 'gemini-3-pro-preview',
|
|
81
|
-
name: 'Gemini 3 Pro (Preview)',
|
|
82
|
-
contextWindow: 1_000_000,
|
|
83
|
-
pricingPerMillionInput: 1.25,
|
|
84
|
-
pricingPerMillionOutput: 10.00,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'gemini-2.5-flash',
|
|
88
|
-
name: 'Gemini 2.5 Flash',
|
|
89
|
-
contextWindow: 1_000_000,
|
|
90
|
-
pricingPerMillionInput: 0.075,
|
|
91
|
-
pricingPerMillionOutput: 0.30,
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 'gemini-2.5-pro',
|
|
95
|
-
name: 'Gemini 2.5 Pro',
|
|
96
|
-
contextWindow: 1_000_000,
|
|
97
|
-
pricingPerMillionInput: 1.25,
|
|
98
|
-
pricingPerMillionOutput: 10.00,
|
|
99
|
-
},
|
|
36
|
+
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash (Preview)', contextWindow: 1_000_000, pricingPerMillionInput: 0.075, pricingPerMillionOutput: 0.3 },
|
|
37
|
+
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro (Preview)', contextWindow: 1_000_000, pricingPerMillionInput: 1.25, pricingPerMillionOutput: 10.0 },
|
|
38
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1_000_000, pricingPerMillionInput: 0.075, pricingPerMillionOutput: 0.3 },
|
|
39
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', contextWindow: 1_000_000, pricingPerMillionInput: 1.25, pricingPerMillionOutput: 10.0 },
|
|
100
40
|
],
|
|
101
41
|
},
|
|
102
42
|
{
|
|
@@ -105,62 +45,63 @@ export const SUPPORTED_MODELS = [
|
|
|
105
45
|
baseURL: 'https://api.cerebras.ai/v1',
|
|
106
46
|
apiKeyEnvVar: 'CEREBRAS_API_KEY',
|
|
107
47
|
models: [
|
|
108
|
-
{
|
|
109
|
-
id: 'llama-4-scout-17b-16e-instruct',
|
|
110
|
-
name: 'Llama 4 Scout 17B',
|
|
111
|
-
contextWindow: 128_000,
|
|
112
|
-
pricingPerMillionInput: 0.00,
|
|
113
|
-
pricingPerMillionOutput: 0.00,
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
id: 'cloudflare-gateway',
|
|
119
|
-
name: 'Cloudflare AI Gateway',
|
|
120
|
-
baseURL: undefined, // Overridden by PROTOAGENT_BASE_URL at runtime
|
|
121
|
-
apiKeyEnvVar: 'PROTOAGENT_API_KEY',
|
|
122
|
-
models: [
|
|
123
|
-
{
|
|
124
|
-
id: 'claude-opus-4-6',
|
|
125
|
-
name: 'Claude Opus 4.6 (via CF Gateway)',
|
|
126
|
-
contextWindow: 200_000,
|
|
127
|
-
pricingPerMillionInput: 5.00,
|
|
128
|
-
pricingPerMillionOutput: 25.00,
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: 'claude-sonnet-4-6',
|
|
132
|
-
name: 'Claude Sonnet 4.6 (via CF Gateway)',
|
|
133
|
-
contextWindow: 200_000,
|
|
134
|
-
pricingPerMillionInput: 3.00,
|
|
135
|
-
pricingPerMillionOutput: 15.00,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
id: 'gpt-4o',
|
|
139
|
-
name: 'GPT-4o (via CF Gateway)',
|
|
140
|
-
contextWindow: 128_000,
|
|
141
|
-
pricingPerMillionInput: 2.50,
|
|
142
|
-
pricingPerMillionOutput: 10.00,
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
id: 'gemini-2.5-flash',
|
|
146
|
-
name: 'Gemini 2.5 Flash (via CF Gateway)',
|
|
147
|
-
contextWindow: 1_000_000,
|
|
148
|
-
pricingPerMillionInput: 0.075,
|
|
149
|
-
pricingPerMillionOutput: 0.30,
|
|
150
|
-
},
|
|
48
|
+
{ id: 'llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 17B', contextWindow: 128_000, pricingPerMillionInput: 0.0, pricingPerMillionOutput: 0.0 },
|
|
151
49
|
],
|
|
152
50
|
},
|
|
153
51
|
];
|
|
154
|
-
|
|
52
|
+
function sanitizeDefaultParams(defaultParams) {
|
|
53
|
+
if (!defaultParams || Object.keys(defaultParams).length === 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
return defaultParams;
|
|
56
|
+
}
|
|
57
|
+
function toProviderMap(providers) {
|
|
58
|
+
return new Map(providers.map((provider) => [provider.id, provider]));
|
|
59
|
+
}
|
|
60
|
+
function mergeModelLists(baseModels, overrideModels) {
|
|
61
|
+
const merged = new Map(baseModels.map((model) => [model.id, model]));
|
|
62
|
+
for (const [modelId, override] of Object.entries(overrideModels || {})) {
|
|
63
|
+
const current = merged.get(modelId);
|
|
64
|
+
merged.set(modelId, {
|
|
65
|
+
id: modelId,
|
|
66
|
+
name: override.name ?? current?.name ?? modelId,
|
|
67
|
+
contextWindow: override.contextWindow ?? current?.contextWindow ?? 0,
|
|
68
|
+
pricingPerMillionInput: override.inputPricePerMillion ?? current?.pricingPerMillionInput ?? 0,
|
|
69
|
+
pricingPerMillionOutput: override.outputPricePerMillion ?? current?.pricingPerMillionOutput ?? 0,
|
|
70
|
+
defaultParams: sanitizeDefaultParams({
|
|
71
|
+
...(current?.defaultParams || {}),
|
|
72
|
+
...(override.defaultParams || {}),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return Array.from(merged.values());
|
|
77
|
+
}
|
|
78
|
+
export function getAllProviders() {
|
|
79
|
+
const runtimeProviders = getRuntimeConfig().providers || {};
|
|
80
|
+
const mergedProviders = toProviderMap(BUILTIN_PROVIDERS);
|
|
81
|
+
for (const [providerId, providerConfig] of Object.entries(runtimeProviders)) {
|
|
82
|
+
const current = mergedProviders.get(providerId);
|
|
83
|
+
mergedProviders.set(providerId, {
|
|
84
|
+
id: providerId,
|
|
85
|
+
name: providerConfig.name ?? current?.name ?? providerId,
|
|
86
|
+
baseURL: providerConfig.baseURL ?? current?.baseURL,
|
|
87
|
+
apiKey: providerConfig.apiKey ?? current?.apiKey,
|
|
88
|
+
apiKeyEnvVar: providerConfig.apiKeyEnvVar ?? current?.apiKeyEnvVar,
|
|
89
|
+
headers: providerConfig.headers ?? current?.headers,
|
|
90
|
+
defaultParams: sanitizeDefaultParams({
|
|
91
|
+
...(current?.defaultParams || {}),
|
|
92
|
+
...(providerConfig.defaultParams || {}),
|
|
93
|
+
}),
|
|
94
|
+
models: mergeModelLists(current?.models || [], providerConfig.models),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return Array.from(mergedProviders.values());
|
|
98
|
+
}
|
|
155
99
|
export function getProvider(providerId) {
|
|
156
|
-
return
|
|
100
|
+
return getAllProviders().find((provider) => provider.id === providerId);
|
|
157
101
|
}
|
|
158
|
-
/** Find a model's details by provider and model ID. */
|
|
159
102
|
export function getModelDetails(providerId, modelId) {
|
|
160
|
-
|
|
161
|
-
return provider?.models.find((m) => m.id === modelId);
|
|
103
|
+
return getProvider(providerId)?.models.find((model) => model.id === modelId);
|
|
162
104
|
}
|
|
163
|
-
/** Get model pricing in per-token format (for cost-tracker). */
|
|
164
105
|
export function getModelPricing(providerId, modelId) {
|
|
165
106
|
const details = getModelDetails(providerId, modelId);
|
|
166
107
|
if (!details)
|
|
@@ -171,3 +112,11 @@ export function getModelPricing(providerId, modelId) {
|
|
|
171
112
|
contextWindow: details.contextWindow,
|
|
172
113
|
};
|
|
173
114
|
}
|
|
115
|
+
export function getRequestDefaultParams(providerId, modelId) {
|
|
116
|
+
const provider = getProvider(providerId);
|
|
117
|
+
const model = getModelDetails(providerId, modelId);
|
|
118
|
+
return {
|
|
119
|
+
...(provider?.defaultParams || {}),
|
|
120
|
+
...(model?.defaultParams || {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parse, printParseErrorCode } from 'jsonc-parser';
|
|
5
|
+
import { logger } from './utils/logger.js';
|
|
6
|
+
const RESERVED_DEFAULT_PARAM_KEYS = new Set([
|
|
7
|
+
'model',
|
|
8
|
+
'messages',
|
|
9
|
+
'tools',
|
|
10
|
+
'tool_choice',
|
|
11
|
+
'stream',
|
|
12
|
+
'stream_options',
|
|
13
|
+
]);
|
|
14
|
+
const DEFAULT_RUNTIME_CONFIG = {
|
|
15
|
+
providers: {},
|
|
16
|
+
mcp: { servers: {} },
|
|
17
|
+
};
|
|
18
|
+
let runtimeConfigCache = null;
|
|
19
|
+
function getRuntimeConfigPaths() {
|
|
20
|
+
const homeDir = os.homedir();
|
|
21
|
+
return [
|
|
22
|
+
path.join(homeDir, '.config', 'protoagent', 'protoagent.jsonc'),
|
|
23
|
+
path.join(homeDir, '.protoagent', 'protoagent.jsonc'),
|
|
24
|
+
path.join(process.cwd(), '.protoagent', 'protoagent.jsonc'),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function interpolateString(value, sourcePath) {
|
|
31
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_match, envVar) => {
|
|
32
|
+
const resolved = process.env[envVar];
|
|
33
|
+
if (resolved === undefined) {
|
|
34
|
+
logger.warn(`Missing environment variable ${envVar} while loading ${sourcePath}`);
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function interpolateValue(value, sourcePath) {
|
|
41
|
+
if (typeof value === 'string') {
|
|
42
|
+
return interpolateString(value, sourcePath);
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.map((entry) => interpolateValue(entry, sourcePath));
|
|
46
|
+
}
|
|
47
|
+
if (isPlainObject(value)) {
|
|
48
|
+
const next = {};
|
|
49
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
50
|
+
const interpolated = interpolateValue(entry, sourcePath);
|
|
51
|
+
if (key === 'headers' && isPlainObject(interpolated)) {
|
|
52
|
+
const filtered = Object.fromEntries(Object.entries(interpolated).filter(([, headerValue]) => typeof headerValue !== 'string' || headerValue.length > 0));
|
|
53
|
+
next[key] = filtered;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
next[key] = interpolated;
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function sanitizeDefaultParamsInConfig(config) {
|
|
63
|
+
const nextProviders = Object.fromEntries(Object.entries(config.providers || {}).map(([providerId, provider]) => {
|
|
64
|
+
const providerDefaultParams = Object.fromEntries(Object.entries(provider.defaultParams || {}).filter(([key]) => {
|
|
65
|
+
const allowed = !RESERVED_DEFAULT_PARAM_KEYS.has(key);
|
|
66
|
+
if (!allowed) {
|
|
67
|
+
logger.warn(`Ignoring reserved provider default param '${key}' for provider ${providerId}`);
|
|
68
|
+
}
|
|
69
|
+
return allowed;
|
|
70
|
+
}));
|
|
71
|
+
const nextModels = Object.fromEntries(Object.entries(provider.models || {}).map(([modelId, model]) => {
|
|
72
|
+
const modelDefaultParams = Object.fromEntries(Object.entries(model.defaultParams || {}).filter(([key]) => {
|
|
73
|
+
const allowed = !RESERVED_DEFAULT_PARAM_KEYS.has(key);
|
|
74
|
+
if (!allowed) {
|
|
75
|
+
logger.warn(`Ignoring reserved model default param '${key}' for model ${providerId}/${modelId}`);
|
|
76
|
+
}
|
|
77
|
+
return allowed;
|
|
78
|
+
}));
|
|
79
|
+
return [
|
|
80
|
+
modelId,
|
|
81
|
+
{
|
|
82
|
+
...model,
|
|
83
|
+
...(Object.keys(modelDefaultParams).length > 0 ? { defaultParams: modelDefaultParams } : {}),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}));
|
|
87
|
+
return [
|
|
88
|
+
providerId,
|
|
89
|
+
{
|
|
90
|
+
...provider,
|
|
91
|
+
...(Object.keys(providerDefaultParams).length > 0 ? { defaultParams: providerDefaultParams } : {}),
|
|
92
|
+
models: nextModels,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}));
|
|
96
|
+
return {
|
|
97
|
+
...config,
|
|
98
|
+
providers: nextProviders,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function mergeRuntimeConfig(base, overlay) {
|
|
102
|
+
const mergedProviders = {
|
|
103
|
+
...(base.providers || {}),
|
|
104
|
+
};
|
|
105
|
+
for (const [providerId, providerConfig] of Object.entries(overlay.providers || {})) {
|
|
106
|
+
const currentProvider = mergedProviders[providerId] || {};
|
|
107
|
+
mergedProviders[providerId] = {
|
|
108
|
+
...currentProvider,
|
|
109
|
+
...providerConfig,
|
|
110
|
+
models: {
|
|
111
|
+
...(currentProvider.models || {}),
|
|
112
|
+
...(providerConfig.models || {}),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const mergedServers = {
|
|
117
|
+
...(base.mcp?.servers || {}),
|
|
118
|
+
};
|
|
119
|
+
for (const [serverName, serverConfig] of Object.entries(overlay.mcp?.servers || {})) {
|
|
120
|
+
const currentServer = mergedServers[serverName];
|
|
121
|
+
mergedServers[serverName] = currentServer && isPlainObject(currentServer)
|
|
122
|
+
? { ...currentServer, ...serverConfig }
|
|
123
|
+
: serverConfig;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
providers: mergedProviders,
|
|
127
|
+
mcp: {
|
|
128
|
+
servers: mergedServers,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function readRuntimeConfigFile(configPath) {
|
|
133
|
+
try {
|
|
134
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
135
|
+
const errors = [];
|
|
136
|
+
const parsed = parse(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
137
|
+
if (errors.length > 0) {
|
|
138
|
+
const details = errors
|
|
139
|
+
.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
|
|
140
|
+
.join(', ');
|
|
141
|
+
throw new Error(`Failed to parse ${configPath}: ${details}`);
|
|
142
|
+
}
|
|
143
|
+
if (!isPlainObject(parsed)) {
|
|
144
|
+
throw new Error(`Failed to parse ${configPath}: top-level value must be an object`);
|
|
145
|
+
}
|
|
146
|
+
return sanitizeDefaultParamsInConfig(interpolateValue(parsed, configPath));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error?.code === 'ENOENT') {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export async function loadRuntimeConfig(forceReload = false) {
|
|
156
|
+
if (!forceReload && runtimeConfigCache) {
|
|
157
|
+
return runtimeConfigCache;
|
|
158
|
+
}
|
|
159
|
+
let merged = DEFAULT_RUNTIME_CONFIG;
|
|
160
|
+
for (const configPath of getRuntimeConfigPaths()) {
|
|
161
|
+
const fileConfig = await readRuntimeConfigFile(configPath);
|
|
162
|
+
if (!fileConfig)
|
|
163
|
+
continue;
|
|
164
|
+
logger.debug(`Loaded runtime config`, { path: configPath });
|
|
165
|
+
merged = mergeRuntimeConfig(merged, fileConfig);
|
|
166
|
+
}
|
|
167
|
+
runtimeConfigCache = merged;
|
|
168
|
+
return merged;
|
|
169
|
+
}
|
|
170
|
+
export function getRuntimeConfig() {
|
|
171
|
+
return runtimeConfigCache || DEFAULT_RUNTIME_CONFIG;
|
|
172
|
+
}
|
|
173
|
+
export function resetRuntimeConfigForTests() {
|
|
174
|
+
runtimeConfigCache = null;
|
|
175
|
+
}
|
package/dist/sub-agent.js
CHANGED
|
@@ -39,7 +39,7 @@ export const subAgentTool = {
|
|
|
39
39
|
* Run a sub-agent with its own isolated conversation.
|
|
40
40
|
* Returns the sub-agent's final text response.
|
|
41
41
|
*/
|
|
42
|
-
export async function runSubAgent(client, model, task, maxIterations = 30) {
|
|
42
|
+
export async function runSubAgent(client, model, task, maxIterations = 30, requestDefaults = {}) {
|
|
43
43
|
const op = logger.startOperation('sub-agent');
|
|
44
44
|
const subAgentSessionId = `sub-agent-${crypto.randomUUID()}`;
|
|
45
45
|
const systemPrompt = await generateSystemPrompt();
|
|
@@ -57,6 +57,7 @@ Do NOT ask the user questions — work autonomously with the tools available.`;
|
|
|
57
57
|
try {
|
|
58
58
|
for (let i = 0; i < maxIterations; i++) {
|
|
59
59
|
const response = await client.chat.completions.create({
|
|
60
|
+
...requestDefaults,
|
|
60
61
|
model,
|
|
61
62
|
messages,
|
|
62
63
|
tools: getAllTools(),
|
package/dist/utils/compactor.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { estimateConversationTokens } from './cost-tracker.js';
|
|
10
10
|
import { logger } from './logger.js';
|
|
11
|
+
import { getTodosForSession } from '../tools/todo.js';
|
|
11
12
|
const RECENT_MESSAGES_TO_KEEP = 5;
|
|
12
13
|
function isProtectedSkillMessage(message) {
|
|
13
14
|
return message.role === 'tool' && typeof message.content === 'string' && message.content.includes('<skill_content ');
|
|
@@ -29,20 +30,20 @@ Be thorough but concise. Do not lose any information that would be needed to con
|
|
|
29
30
|
* Compact a conversation if it exceeds the context window threshold.
|
|
30
31
|
* Returns the original messages if compaction isn't needed or fails.
|
|
31
32
|
*/
|
|
32
|
-
export async function compactIfNeeded(client, model, messages, contextWindow, currentTokens) {
|
|
33
|
+
export async function compactIfNeeded(client, model, messages, contextWindow, currentTokens, requestDefaults = {}, sessionId) {
|
|
33
34
|
const utilisation = (currentTokens / contextWindow) * 100;
|
|
34
35
|
if (utilisation < 90)
|
|
35
36
|
return messages;
|
|
36
37
|
logger.info(`Compacting conversation (${utilisation.toFixed(1)}% of context window used)`);
|
|
37
38
|
try {
|
|
38
|
-
return await compactConversation(client, model, messages);
|
|
39
|
+
return await compactConversation(client, model, messages, requestDefaults, sessionId);
|
|
39
40
|
}
|
|
40
41
|
catch (err) {
|
|
41
42
|
logger.error(`Compaction failed, continuing with original messages: ${err}`);
|
|
42
43
|
return messages;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
async function compactConversation(client, model, messages) {
|
|
46
|
+
async function compactConversation(client, model, messages, _requestDefaults, sessionId) {
|
|
46
47
|
// Separate system message, history to compress, and recent messages
|
|
47
48
|
const systemMessage = messages[0];
|
|
48
49
|
const recentMessages = messages.slice(-RECENT_MESSAGES_TO_KEEP);
|
|
@@ -54,13 +55,17 @@ async function compactConversation(client, model, messages) {
|
|
|
54
55
|
return messages;
|
|
55
56
|
}
|
|
56
57
|
// Build compression request
|
|
58
|
+
const activeTodos = getTodosForSession(sessionId);
|
|
59
|
+
const todoReminder = activeTodos.length > 0
|
|
60
|
+
? `\n\nActive TODOs:\n${activeTodos.map((todo) => `- [${todo.status}] ${todo.content}`).join('\n')}\n\nThe agent must not stop until the TODO list is fully complete. Preserve that instruction in the summary if work remains.`
|
|
61
|
+
: '';
|
|
57
62
|
const compressionMessages = [
|
|
58
63
|
{ role: 'system', content: COMPRESSION_PROMPT },
|
|
59
64
|
{
|
|
60
65
|
role: 'user',
|
|
61
66
|
content: `Here is the conversation history to compress:\n\n${historyToCompress
|
|
62
67
|
.map((m) => `[${m.role}]: ${m.content || JSON.stringify(m.tool_calls || '')}`)
|
|
63
|
-
.join('\n\n')}`,
|
|
68
|
+
.join('\n\n')}${todoReminder}`,
|
|
64
69
|
},
|
|
65
70
|
];
|
|
66
71
|
const response = await client.chat.completions.create({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protoagent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"html-to-text": "^9.0.5",
|
|
31
31
|
"ink": "^6.7.0",
|
|
32
32
|
"ink-big-text": "^2.0.0",
|
|
33
|
+
"jsonc-parser": "^3.3.1",
|
|
33
34
|
"openai": "^5.23.1",
|
|
34
35
|
"react": "^19.1.1",
|
|
35
36
|
"turndown": "^7.2.2",
|