protoagent 0.1.1 → 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 +40 -8
- package/dist/agentic-loop.js +4 -2
- package/dist/components/ConfigDialog.js +6 -4
- package/dist/config.js +48 -16
- package/dist/mcp.js +20 -19
- package/dist/providers.js +73 -88
- 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';
|
|
@@ -137,8 +138,31 @@ function buildClient(config) {
|
|
|
137
138
|
const clientOptions = {
|
|
138
139
|
apiKey,
|
|
139
140
|
};
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
// baseURL: env var override takes precedence over provider default
|
|
142
|
+
const baseURLOverride = process.env.PROTOAGENT_BASE_URL?.trim();
|
|
143
|
+
const baseURL = baseURLOverride || provider?.baseURL;
|
|
144
|
+
if (baseURL) {
|
|
145
|
+
clientOptions.baseURL = baseURL;
|
|
146
|
+
}
|
|
147
|
+
// Custom headers: env override takes precedence over provider defaults
|
|
148
|
+
const rawHeaders = process.env.PROTOAGENT_CUSTOM_HEADERS?.trim();
|
|
149
|
+
if (rawHeaders) {
|
|
150
|
+
const defaultHeaders = {};
|
|
151
|
+
for (const line of rawHeaders.split('\n')) {
|
|
152
|
+
const sep = line.indexOf(': ');
|
|
153
|
+
if (sep === -1)
|
|
154
|
+
continue;
|
|
155
|
+
const key = line.slice(0, sep).trim();
|
|
156
|
+
const value = line.slice(sep + 2).trim();
|
|
157
|
+
if (key && value)
|
|
158
|
+
defaultHeaders[key] = value;
|
|
159
|
+
}
|
|
160
|
+
if (Object.keys(defaultHeaders).length > 0) {
|
|
161
|
+
clientOptions.defaultHeaders = defaultHeaders;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (provider?.headers && Object.keys(provider.headers).length > 0) {
|
|
165
|
+
clientOptions.defaultHeaders = provider.headers;
|
|
142
166
|
}
|
|
143
167
|
return new OpenAI(clientOptions);
|
|
144
168
|
}
|
|
@@ -176,7 +200,7 @@ const InlineSetup = ({ onComplete }) => {
|
|
|
176
200
|
const [selectedProviderId, setSelectedProviderId] = useState('');
|
|
177
201
|
const [selectedModelId, setSelectedModelId] = useState('');
|
|
178
202
|
const [apiKeyError, setApiKeyError] = useState('');
|
|
179
|
-
const providerItems =
|
|
203
|
+
const providerItems = getAllProviders().flatMap((provider) => provider.models.map((model) => ({
|
|
180
204
|
label: `${provider.name} - ${model.name}`,
|
|
181
205
|
value: `${provider.id}:::${model.id}`,
|
|
182
206
|
})));
|
|
@@ -189,15 +213,16 @@ const InlineSetup = ({ onComplete }) => {
|
|
|
189
213
|
} }) })] }));
|
|
190
214
|
}
|
|
191
215
|
const provider = getProvider(selectedProviderId);
|
|
192
|
-
|
|
193
|
-
|
|
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) {
|
|
194
219
|
setApiKeyError('API key cannot be empty.');
|
|
195
220
|
return;
|
|
196
221
|
}
|
|
197
222
|
const newConfig = {
|
|
198
223
|
provider: selectedProviderId,
|
|
199
224
|
model: selectedModelId,
|
|
200
|
-
apiKey: value.trim(),
|
|
225
|
+
...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
|
|
201
226
|
};
|
|
202
227
|
writeConfig(newConfig);
|
|
203
228
|
onComplete(newConfig);
|
|
@@ -311,6 +336,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
311
336
|
setPendingApproval({ request: req, resolve });
|
|
312
337
|
});
|
|
313
338
|
});
|
|
339
|
+
await loadRuntimeConfig();
|
|
314
340
|
// Load config — if none exists, show inline setup
|
|
315
341
|
const loadedConfig = readConfig();
|
|
316
342
|
if (!loadedConfig) {
|
|
@@ -416,6 +442,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
416
442
|
assistantMessageRef.current = null;
|
|
417
443
|
try {
|
|
418
444
|
const pricing = getModelPricing(config.provider, config.model);
|
|
445
|
+
const requestDefaults = getRequestDefaultParams(config.provider, config.model);
|
|
419
446
|
// Create abort controller for this completion
|
|
420
447
|
abortControllerRef.current = new AbortController();
|
|
421
448
|
const updatedMessages = await runAgenticLoop(clientRef.current, config.model, [...completionMessages, userMessage], trimmed, (event) => {
|
|
@@ -548,7 +575,12 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
548
575
|
setThreadErrors((prev) => prev.filter((threadError) => !threadError.transient));
|
|
549
576
|
break;
|
|
550
577
|
}
|
|
551
|
-
}, {
|
|
578
|
+
}, {
|
|
579
|
+
pricing: pricing || undefined,
|
|
580
|
+
abortSignal: abortControllerRef.current.signal,
|
|
581
|
+
sessionId: session?.id,
|
|
582
|
+
requestDefaults,
|
|
583
|
+
});
|
|
552
584
|
// Final update to ensure we have the complete message history
|
|
553
585
|
setCompletionMessages(updatedMessages);
|
|
554
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) {
|
|
@@ -19,11 +20,34 @@ export function resolveApiKey(config) {
|
|
|
19
20
|
return directApiKey;
|
|
20
21
|
}
|
|
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
|
+
}
|
|
37
|
+
// Fallback for Cloudflare Gateway or other custom header setups
|
|
38
|
+
if (process.env.PROTOAGENT_CUSTOM_HEADERS) {
|
|
39
|
+
return 'none';
|
|
40
|
+
}
|
|
22
41
|
if (!provider?.apiKeyEnvVar) {
|
|
42
|
+
if (provider?.headers && Object.keys(provider.headers).length > 0) {
|
|
43
|
+
return 'none';
|
|
44
|
+
}
|
|
23
45
|
return null;
|
|
24
46
|
}
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
if (provider?.headers && Object.keys(provider.headers).length > 0) {
|
|
48
|
+
return 'none';
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
27
51
|
}
|
|
28
52
|
export const getConfigDirectory = () => {
|
|
29
53
|
const homeDir = os.homedir();
|
|
@@ -51,8 +75,8 @@ export const readConfig = () => {
|
|
|
51
75
|
// Handle legacy format: { provider, model, credentials: { KEY: "..." } }
|
|
52
76
|
let apiKey = raw.apiKey;
|
|
53
77
|
if (!apiKey && raw.credentials && typeof raw.credentials === 'object') {
|
|
54
|
-
const provider =
|
|
55
|
-
if (provider) {
|
|
78
|
+
const provider = getAllProviders().find((p) => p.id === raw.provider);
|
|
79
|
+
if (provider?.apiKeyEnvVar) {
|
|
56
80
|
apiKey = raw.credentials[provider.apiKeyEnvVar];
|
|
57
81
|
}
|
|
58
82
|
// Fallback: grab the first non-empty value
|
|
@@ -89,14 +113,21 @@ export const writeConfig = (config) => {
|
|
|
89
113
|
};
|
|
90
114
|
export const InitialLoading = ({ setExistingConfig, setStep }) => {
|
|
91
115
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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);
|
|
98
129
|
setStep(2);
|
|
99
|
-
}
|
|
130
|
+
});
|
|
100
131
|
}, []);
|
|
101
132
|
return _jsx(Text, { children: "Loading configuration..." });
|
|
102
133
|
};
|
|
@@ -114,7 +145,7 @@ export const ResetPrompt = ({ existingConfig, setStep, setConfigWritten }) => {
|
|
|
114
145
|
} })] }));
|
|
115
146
|
};
|
|
116
147
|
export const ModelSelection = ({ setSelectedProviderId, setSelectedModelId, setStep, }) => {
|
|
117
|
-
const items =
|
|
148
|
+
const items = getAllProviders().flatMap((provider) => provider.models.map((model) => ({
|
|
118
149
|
label: `${provider.name} - ${model.name}`,
|
|
119
150
|
value: `${provider.id}:::${model.id}`,
|
|
120
151
|
})));
|
|
@@ -129,21 +160,22 @@ export const ModelSelection = ({ setSelectedProviderId, setSelectedModelId, setS
|
|
|
129
160
|
export const ApiKeyInput = ({ selectedProviderId, selectedModelId, setStep, setConfigWritten, }) => {
|
|
130
161
|
const [errorMessage, setErrorMessage] = useState('');
|
|
131
162
|
const provider = getProvider(selectedProviderId);
|
|
163
|
+
const canUseResolvedAuth = Boolean(resolveApiKey({ provider: selectedProviderId, apiKey: undefined }));
|
|
132
164
|
const handleApiKeySubmit = (value) => {
|
|
133
|
-
if (value.trim().length === 0) {
|
|
165
|
+
if (value.trim().length === 0 && !canUseResolvedAuth) {
|
|
134
166
|
setErrorMessage('API key cannot be empty.');
|
|
135
167
|
return;
|
|
136
168
|
}
|
|
137
169
|
const newConfig = {
|
|
138
170
|
provider: selectedProviderId,
|
|
139
171
|
model: selectedModelId,
|
|
140
|
-
apiKey: value.trim(),
|
|
172
|
+
...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
|
|
141
173
|
};
|
|
142
174
|
writeConfig(newConfig);
|
|
143
175
|
setConfigWritten(true);
|
|
144
176
|
setStep(4);
|
|
145
177
|
};
|
|
146
|
-
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 })] }));
|
|
147
179
|
};
|
|
148
180
|
export const ConfigResult = ({ configWritten }) => {
|
|
149
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,26 +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
|
-
},
|
|
48
|
+
{ id: 'llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 17B', contextWindow: 128_000, pricingPerMillionInput: 0.0, pricingPerMillionOutput: 0.0 },
|
|
115
49
|
],
|
|
116
50
|
},
|
|
117
51
|
];
|
|
118
|
-
|
|
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
|
+
}
|
|
119
99
|
export function getProvider(providerId) {
|
|
120
|
-
return
|
|
100
|
+
return getAllProviders().find((provider) => provider.id === providerId);
|
|
121
101
|
}
|
|
122
|
-
/** Find a model's details by provider and model ID. */
|
|
123
102
|
export function getModelDetails(providerId, modelId) {
|
|
124
|
-
|
|
125
|
-
return provider?.models.find((m) => m.id === modelId);
|
|
103
|
+
return getProvider(providerId)?.models.find((model) => model.id === modelId);
|
|
126
104
|
}
|
|
127
|
-
/** Get model pricing in per-token format (for cost-tracker). */
|
|
128
105
|
export function getModelPricing(providerId, modelId) {
|
|
129
106
|
const details = getModelDetails(providerId, modelId);
|
|
130
107
|
if (!details)
|
|
@@ -135,3 +112,11 @@ export function getModelPricing(providerId, modelId) {
|
|
|
135
112
|
contextWindow: details.contextWindow,
|
|
136
113
|
};
|
|
137
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",
|