@librechat/agents 3.6.12 → 3.6.14
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/cjs/agents/AgentContext.cjs +4 -0
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +1 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/contextPressureMeter.cjs +84 -4
- package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
- package/dist/cjs/llm/tokenCounterCacheCompatibility.cjs +14 -0
- package/dist/cjs/llm/tokenCounterCacheCompatibility.cjs.map +1 -0
- package/dist/cjs/main.cjs +8 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +33 -14
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +3 -2
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +10 -14
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +67 -3
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/syntaxCheck.cjs +10 -14
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
- package/dist/cjs/tools/local/workspaceFS.cjs +2 -2
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +3 -2
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +4 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +1 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/contextPressureMeter.mjs +84 -5
- package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
- package/dist/esm/llm/tokenCounterCacheCompatibility.mjs +13 -0
- package/dist/esm/llm/tokenCounterCacheCompatibility.mjs.map +1 -0
- package/dist/esm/main.mjs +4 -3
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +33 -15
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +4 -3
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +11 -15
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +63 -4
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/syntaxCheck.mjs +11 -15
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
- package/dist/esm/tools/local/workspaceFS.mjs +2 -2
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +3 -2
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/contextPressureMeter.d.ts +7 -1
- package/dist/types/llm/tokenCounterCacheCompatibility.d.ts +7 -0
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +6 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +22 -0
- package/dist/types/tools/local/workspaceFS.d.ts +1 -1
- package/dist/types/types/tools.d.ts +22 -37
- package/package.json +3 -1
- package/src/agents/AgentContext.ts +9 -0
- package/src/graphs/Graph.ts +1 -0
- package/src/index.ts +1 -0
- package/src/llm/contextPressureMeter.ts +169 -4
- package/src/llm/tokenCounterCacheCompatibility.ts +22 -0
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +68 -17
- package/src/tools/cloudflare/CloudflareSandboxTools.ts +4 -3
- package/src/tools/local/LocalCodingTools.ts +25 -35
- package/src/tools/local/LocalExecutionEngine.ts +120 -3
- package/src/tools/local/syntaxCheck.ts +23 -26
- package/src/tools/local/workspaceFS.ts +2 -3
- package/src/types/tools.ts +25 -37
- package/src/utils/tokens.ts +3 -2
package/src/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export {
|
|
|
69
69
|
export type { Interrupt } from '@langchain/langgraph';
|
|
70
70
|
|
|
71
71
|
/* LLM */
|
|
72
|
+
export { markTokenCounterCacheCompatible } from './llm/tokenCounterCacheCompatibility';
|
|
72
73
|
export { CustomOpenAIClient } from './llm/openai';
|
|
73
74
|
export { ChatOpenRouter } from './llm/openrouter';
|
|
74
75
|
export type {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isProxy } from 'node:util/types';
|
|
1
2
|
import { ToolMessage } from '@langchain/core/messages';
|
|
2
3
|
|
|
3
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
|
+
import type { AIMessage, BaseMessage } from '@langchain/core/messages';
|
|
4
5
|
import type { ProviderPayloadMeasurement } from '@/llm/prepareProviderRequest';
|
|
5
6
|
import type * as t from '@/types';
|
|
6
7
|
|
|
@@ -19,6 +20,7 @@ interface ContextPressureUsage {
|
|
|
19
20
|
|
|
20
21
|
interface ContextPressureMeterParams {
|
|
21
22
|
tokenCounter?: t.TokenCounter;
|
|
23
|
+
tokenCountCache?: ExactTokenCountCache;
|
|
22
24
|
sourceMessages: BaseMessage[];
|
|
23
25
|
retainedMessages: BaseMessage[];
|
|
24
26
|
indexTokenCountMap: Record<string, number | undefined>;
|
|
@@ -51,6 +53,164 @@ export interface ContextPressureMeter {
|
|
|
51
53
|
): ProviderPayloadMeasurement;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
export interface ExactTokenCountCache {
|
|
57
|
+
count(message: BaseMessage): number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface StableTokenSurface {
|
|
61
|
+
content: string;
|
|
62
|
+
messageType: string;
|
|
63
|
+
role?: string;
|
|
64
|
+
additionalType?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ExactTokenCountCacheEntry {
|
|
68
|
+
surface: StableTokenSurface;
|
|
69
|
+
tokens: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readDataProperty(
|
|
73
|
+
owner: object,
|
|
74
|
+
property: PropertyKey
|
|
75
|
+
): { own: boolean; safe: boolean; value?: unknown } {
|
|
76
|
+
let descriptor: PropertyDescriptor | undefined;
|
|
77
|
+
try {
|
|
78
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, property);
|
|
79
|
+
} catch {
|
|
80
|
+
return { own: false, safe: false };
|
|
81
|
+
}
|
|
82
|
+
if (descriptor == null) {
|
|
83
|
+
return { own: false, safe: true };
|
|
84
|
+
}
|
|
85
|
+
return 'value' in descriptor
|
|
86
|
+
? { own: true, safe: true, value: descriptor.value }
|
|
87
|
+
: { own: true, safe: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getStableTokenSurface(
|
|
91
|
+
message: BaseMessage
|
|
92
|
+
): StableTokenSurface | undefined {
|
|
93
|
+
if (isProxy(message)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const contentProperty = readDataProperty(message, 'content');
|
|
97
|
+
if (
|
|
98
|
+
!contentProperty.safe ||
|
|
99
|
+
!contentProperty.own ||
|
|
100
|
+
typeof contentProperty.value !== 'string'
|
|
101
|
+
) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const messageType = message.getType();
|
|
105
|
+
const roleProperty = readDataProperty(message, 'role');
|
|
106
|
+
if (!roleProperty.safe || (!roleProperty.own && 'role' in message)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const role = roleProperty.value;
|
|
110
|
+
if (role != null && typeof role !== 'string') {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const additionalKwargsProperty = readDataProperty(
|
|
115
|
+
message,
|
|
116
|
+
'additional_kwargs'
|
|
117
|
+
);
|
|
118
|
+
if (!additionalKwargsProperty.safe || !additionalKwargsProperty.own) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const rawAdditionalKwargs = additionalKwargsProperty.value;
|
|
122
|
+
if (
|
|
123
|
+
rawAdditionalKwargs == null ||
|
|
124
|
+
typeof rawAdditionalKwargs !== 'object' ||
|
|
125
|
+
isProxy(rawAdditionalKwargs)
|
|
126
|
+
) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const additionalKwargs = rawAdditionalKwargs;
|
|
130
|
+
const typeProperty = readDataProperty(additionalKwargs, 'type');
|
|
131
|
+
if (!typeProperty.safe) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
if (messageType === 'ai' || role === 'assistant') {
|
|
135
|
+
const toolCallsProperty = readDataProperty(
|
|
136
|
+
message as AIMessage,
|
|
137
|
+
'tool_calls'
|
|
138
|
+
);
|
|
139
|
+
if (
|
|
140
|
+
!toolCallsProperty.safe ||
|
|
141
|
+
(!toolCallsProperty.own && 'tool_calls' in message)
|
|
142
|
+
) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const toolCalls = toolCallsProperty.value;
|
|
146
|
+
if (
|
|
147
|
+
toolCalls != null &&
|
|
148
|
+
(!Array.isArray(toolCalls) || isProxy(toolCalls) || toolCalls.length > 0)
|
|
149
|
+
) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const functionCall = readDataProperty(additionalKwargs, 'function_call');
|
|
153
|
+
if (!functionCall.safe || functionCall.value != null) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let additionalType: string | undefined;
|
|
159
|
+
if (messageType === 'tool') {
|
|
160
|
+
const typeValue = typeProperty.value;
|
|
161
|
+
if (typeValue != null && typeof typeValue !== 'string') {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
additionalType = typeof typeValue === 'string' ? typeValue : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: contentProperty.value,
|
|
169
|
+
messageType,
|
|
170
|
+
...(role != null && { role }),
|
|
171
|
+
...(additionalType != null && { additionalType }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function tokenSurfacesMatch(
|
|
176
|
+
left: StableTokenSurface,
|
|
177
|
+
right: StableTokenSurface
|
|
178
|
+
): boolean {
|
|
179
|
+
return (
|
|
180
|
+
left.content === right.content &&
|
|
181
|
+
left.messageType === right.messageType &&
|
|
182
|
+
left.role === right.role &&
|
|
183
|
+
left.additionalType === right.additionalType
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reuses exact counts only while every token-relevant stable field matches. */
|
|
188
|
+
export function createExactTokenCountCache(
|
|
189
|
+
tokenCounter: t.TokenCounter
|
|
190
|
+
): ExactTokenCountCache {
|
|
191
|
+
const entries = new WeakMap<BaseMessage, ExactTokenCountCacheEntry>();
|
|
192
|
+
return {
|
|
193
|
+
count(message): number {
|
|
194
|
+
const surface = getStableTokenSurface(message);
|
|
195
|
+
const cached = entries.get(message);
|
|
196
|
+
if (
|
|
197
|
+
surface != null &&
|
|
198
|
+
cached != null &&
|
|
199
|
+
tokenSurfacesMatch(cached.surface, surface)
|
|
200
|
+
) {
|
|
201
|
+
return cached.tokens;
|
|
202
|
+
}
|
|
203
|
+
const tokens = tokenCounter(message);
|
|
204
|
+
if (surface != null) {
|
|
205
|
+
entries.set(message, { surface, tokens });
|
|
206
|
+
} else {
|
|
207
|
+
entries.delete(message);
|
|
208
|
+
}
|
|
209
|
+
return tokens;
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
54
214
|
function getProviderMessageOriginKey(message: BaseMessage): string | undefined {
|
|
55
215
|
const type = message.getType();
|
|
56
216
|
if (
|
|
@@ -69,6 +229,7 @@ function getProviderMessageOriginKey(message: BaseMessage): string | undefined {
|
|
|
69
229
|
/** Measures repeated provider projections while tokenizing each message object once. */
|
|
70
230
|
export function createContextPressureMeter({
|
|
71
231
|
tokenCounter,
|
|
232
|
+
tokenCountCache,
|
|
72
233
|
sourceMessages,
|
|
73
234
|
retainedMessages,
|
|
74
235
|
indexTokenCountMap,
|
|
@@ -88,12 +249,16 @@ export function createContextPressureMeter({
|
|
|
88
249
|
if (cached != null) {
|
|
89
250
|
return cached;
|
|
90
251
|
}
|
|
91
|
-
const tokens =
|
|
252
|
+
const tokens =
|
|
253
|
+
tokenCountCache?.count(message) ?? tokenCounter?.(message) ?? 0;
|
|
92
254
|
tokenCounts.set(message, tokens);
|
|
93
255
|
return tokens;
|
|
94
256
|
};
|
|
95
257
|
|
|
96
|
-
if (
|
|
258
|
+
if (
|
|
259
|
+
contextUsage != null &&
|
|
260
|
+
(tokenCounter != null || tokenCountCache != null)
|
|
261
|
+
) {
|
|
97
262
|
const sourceIndices = new WeakMap<BaseMessage, number>();
|
|
98
263
|
for (let i = 0; i < sourceMessages.length; i++) {
|
|
99
264
|
sourceIndices.set(sourceMessages[i], i);
|
|
@@ -179,7 +344,7 @@ export function createContextPressureMeter({
|
|
|
179
344
|
contextUsage?.effectiveInstructionTokens ??
|
|
180
345
|
(forceRawRecount ? instructionTokens : undefined);
|
|
181
346
|
if (
|
|
182
|
-
tokenCounter == null ||
|
|
347
|
+
(tokenCounter == null && tokenCountCache == null) ||
|
|
183
348
|
contextBudget == null ||
|
|
184
349
|
effectiveInstructionTokens == null
|
|
185
350
|
) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
|
|
3
|
+
const stableMessageTokenCounters = new WeakSet<
|
|
4
|
+
(message: BaseMessage) => number
|
|
5
|
+
>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Opts a deterministic counter into stable-message count reuse. The counter
|
|
9
|
+
* must depend only on the message surface consumed by `getTokenCountForMessage`.
|
|
10
|
+
*/
|
|
11
|
+
export function markTokenCounterCacheCompatible<
|
|
12
|
+
TCounter extends (message: BaseMessage) => number,
|
|
13
|
+
>(tokenCounter: TCounter): TCounter {
|
|
14
|
+
stableMessageTokenCounters.add(tokenCounter);
|
|
15
|
+
return tokenCounter;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isTokenCounterCacheCompatible(
|
|
19
|
+
tokenCounter: (message: BaseMessage) => number
|
|
20
|
+
): boolean {
|
|
21
|
+
return stableMessageTokenCounters.has(tokenCounter);
|
|
22
|
+
}
|
|
@@ -5,11 +5,11 @@ import type { WriteFileOptions, MakeDirectoryOptions, Stats } from 'fs';
|
|
|
5
5
|
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
|
6
6
|
import type { FileHandle } from 'fs/promises';
|
|
7
7
|
import type { WorkspaceFS, ReaddirEntry } from '@/tools/local/workspaceFS';
|
|
8
|
+
import type * as t from '@/types';
|
|
8
9
|
import {
|
|
9
10
|
WorkspaceClientTimeoutError,
|
|
10
11
|
isWorkspaceClientTimeoutError,
|
|
11
12
|
} from '@/tools/local/workspaceFS';
|
|
12
|
-
import type * as t from '@/types';
|
|
13
13
|
import {
|
|
14
14
|
LOCAL_SPAWN_TIMEOUT_MS,
|
|
15
15
|
validateBashCommand,
|
|
@@ -44,9 +44,28 @@ type SandboxRuntimeContext = {
|
|
|
44
44
|
shell: string;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
type ExecutionWorldCacheEntry = {
|
|
48
|
+
world: t.ExecutionWorld;
|
|
49
|
+
workspaceRoot: string;
|
|
50
|
+
timeoutMs: number;
|
|
51
|
+
sandbox: t.CloudflareSandboxExecutionConfig['sandbox'];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const executionWorldCache = new WeakMap<
|
|
55
|
+
t.CloudflareSandboxExecutionConfig,
|
|
56
|
+
ExecutionWorldCacheEntry
|
|
57
|
+
>();
|
|
58
|
+
|
|
59
|
+
type SandboxFactoryCacheEntry = {
|
|
60
|
+
sandbox: () =>
|
|
61
|
+
| t.CloudflareSandboxRuntime
|
|
62
|
+
| Promise<t.CloudflareSandboxRuntime>;
|
|
63
|
+
promise: Promise<t.CloudflareSandboxRuntime>;
|
|
64
|
+
};
|
|
65
|
+
|
|
47
66
|
const sandboxFactoryCache = new WeakMap<
|
|
48
67
|
t.CloudflareSandboxExecutionConfig,
|
|
49
|
-
|
|
68
|
+
SandboxFactoryCacheEntry
|
|
50
69
|
>();
|
|
51
70
|
|
|
52
71
|
function normalizeWorkspaceRoot(workspaceRoot: string): string {
|
|
@@ -69,17 +88,20 @@ export async function resolveCloudflareSandbox(
|
|
|
69
88
|
if (typeof sandbox !== 'function') {
|
|
70
89
|
return sandbox;
|
|
71
90
|
}
|
|
72
|
-
|
|
73
|
-
if (cached
|
|
74
|
-
cached
|
|
75
|
-
.then(() => sandbox())
|
|
76
|
-
.catch((error: unknown) => {
|
|
77
|
-
sandboxFactoryCache.delete(config);
|
|
78
|
-
throw error;
|
|
79
|
-
});
|
|
80
|
-
sandboxFactoryCache.set(config, cached);
|
|
91
|
+
const cached = sandboxFactoryCache.get(config);
|
|
92
|
+
if (cached?.sandbox === sandbox) {
|
|
93
|
+
return cached.promise;
|
|
81
94
|
}
|
|
82
|
-
|
|
95
|
+
const promise = Promise.resolve()
|
|
96
|
+
.then(() => sandbox())
|
|
97
|
+
.catch((error: unknown) => {
|
|
98
|
+
if (sandboxFactoryCache.get(config)?.promise === promise) {
|
|
99
|
+
sandboxFactoryCache.delete(config);
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
});
|
|
103
|
+
sandboxFactoryCache.set(config, { sandbox, promise });
|
|
104
|
+
return promise;
|
|
83
105
|
}
|
|
84
106
|
|
|
85
107
|
async function getRuntimeContext(
|
|
@@ -687,6 +709,39 @@ function createCloudflareSpawn(
|
|
|
687
709
|
};
|
|
688
710
|
}
|
|
689
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Returns one stable filesystem/process world for a Cloudflare configuration.
|
|
714
|
+
* Probe caches key on the spawn identity, so retaining it avoids repeating
|
|
715
|
+
* remote capability checks whenever an agent binding rebuilds its tools.
|
|
716
|
+
*/
|
|
717
|
+
export function createCloudflareExecutionWorld(
|
|
718
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
719
|
+
): t.ExecutionWorld {
|
|
720
|
+
const workspaceRoot = getCloudflareWorkspaceRoot(config);
|
|
721
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
722
|
+
const cached = executionWorldCache.get(config);
|
|
723
|
+
if (
|
|
724
|
+
cached?.workspaceRoot === workspaceRoot &&
|
|
725
|
+
Object.is(cached.timeoutMs, timeoutMs) &&
|
|
726
|
+
cached.sandbox === config.sandbox
|
|
727
|
+
) {
|
|
728
|
+
return cached.world;
|
|
729
|
+
}
|
|
730
|
+
const fs = Object.freeze(createCloudflareWorkspaceFS(config));
|
|
731
|
+
const world = Object.freeze({
|
|
732
|
+
spawn: createCloudflareSpawn(config),
|
|
733
|
+
fs,
|
|
734
|
+
sandboxed: true,
|
|
735
|
+
});
|
|
736
|
+
executionWorldCache.set(config, {
|
|
737
|
+
world,
|
|
738
|
+
workspaceRoot,
|
|
739
|
+
timeoutMs,
|
|
740
|
+
sandbox: config.sandbox,
|
|
741
|
+
});
|
|
742
|
+
return world;
|
|
743
|
+
}
|
|
744
|
+
|
|
690
745
|
export function createCloudflareLocalExecutionConfig(
|
|
691
746
|
config: t.CloudflareSandboxExecutionConfig
|
|
692
747
|
): t.LocalExecutionConfig {
|
|
@@ -694,11 +749,7 @@ export function createCloudflareLocalExecutionConfig(
|
|
|
694
749
|
return {
|
|
695
750
|
cwd: workspaceRoot,
|
|
696
751
|
workspace: { root: workspaceRoot },
|
|
697
|
-
exec:
|
|
698
|
-
spawn: createCloudflareSpawn(config),
|
|
699
|
-
fs: createCloudflareWorkspaceFS(config),
|
|
700
|
-
sandboxed: true,
|
|
701
|
-
},
|
|
752
|
+
exec: createCloudflareExecutionWorld(config),
|
|
702
753
|
shell: config.shell ?? 'bash',
|
|
703
754
|
timeoutMs: config.timeoutMs,
|
|
704
755
|
maxOutputChars: config.maxOutputChars,
|
|
@@ -2,8 +2,8 @@ import { tool } from '@langchain/core/tools';
|
|
|
2
2
|
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
|
3
3
|
import type * as t from '@/types';
|
|
4
4
|
import {
|
|
5
|
+
createCloudflareExecutionWorld,
|
|
5
6
|
createCloudflareLocalExecutionConfig,
|
|
6
|
-
createCloudflareWorkspaceFS,
|
|
7
7
|
executeCloudflareBash,
|
|
8
8
|
executeCloudflareCode,
|
|
9
9
|
formatCloudflareOutput,
|
|
@@ -161,7 +161,7 @@ export function createCloudflareCodingTools(
|
|
|
161
161
|
options.checkpointer ??
|
|
162
162
|
(config.fileCheckpointing === true
|
|
163
163
|
? createLocalFileCheckpointer({
|
|
164
|
-
fs:
|
|
164
|
+
fs: createCloudflareExecutionWorld(config).fs,
|
|
165
165
|
})
|
|
166
166
|
: undefined);
|
|
167
167
|
const tools = [
|
|
@@ -193,11 +193,12 @@ export function createCloudflareCodingToolBundle(
|
|
|
193
193
|
config: t.CloudflareSandboxExecutionConfig,
|
|
194
194
|
options: { checkpointer?: t.LocalFileCheckpointer } = {}
|
|
195
195
|
): CloudflareCodingToolBundle {
|
|
196
|
+
const world = createCloudflareExecutionWorld(config);
|
|
196
197
|
const checkpointer =
|
|
197
198
|
options.checkpointer ??
|
|
198
199
|
(config.fileCheckpointing === true
|
|
199
200
|
? createLocalFileCheckpointer({
|
|
200
|
-
fs:
|
|
201
|
+
fs: world.fs,
|
|
201
202
|
})
|
|
202
203
|
: undefined);
|
|
203
204
|
return {
|
|
@@ -4,16 +4,19 @@ import { tool } from '@langchain/core/tools';
|
|
|
4
4
|
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
|
5
5
|
import type * as t from '@/types';
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
createLocalProgrammaticToolCallingTool,
|
|
9
|
-
} from './LocalProgrammaticToolCalling';
|
|
10
|
-
import {
|
|
7
|
+
commandAvailabilityEnvCacheKey,
|
|
11
8
|
getSpawn,
|
|
12
9
|
getWorkspaceFS,
|
|
10
|
+
probeLocalCommandAvailability,
|
|
13
11
|
resolveWorkspacePathSafe,
|
|
12
|
+
setCommandAvailabilityCacheEntry,
|
|
14
13
|
spawnLocalProcess,
|
|
15
14
|
truncateLocalOutput,
|
|
16
15
|
} from './LocalExecutionEngine';
|
|
16
|
+
import {
|
|
17
|
+
createLocalBashProgrammaticToolCallingTool,
|
|
18
|
+
createLocalProgrammaticToolCallingTool,
|
|
19
|
+
} from './LocalProgrammaticToolCalling';
|
|
17
20
|
import {
|
|
18
21
|
createLocalBashExecutionTool,
|
|
19
22
|
createLocalCodeExecutionTool,
|
|
@@ -736,30 +739,14 @@ export function createLocalEditFileTool(
|
|
|
736
739
|
* spawn-per-search.
|
|
737
740
|
*/
|
|
738
741
|
// Per-backend × per-env cache. Codex P1 #34 — keying by spawn
|
|
739
|
-
// backend alone misses the case where two Runs share a backend but
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// instead of falling back to the Node walker. The inner Map is
|
|
743
|
-
// keyed by a stable JSON hash of the effective env so each unique
|
|
744
|
-
// env gets its own probe.
|
|
742
|
+
// backend alone misses the case where two Runs share a backend but vary
|
|
743
|
+
// `local.env`. The inner map uses non-plaintext environment hashes and is
|
|
744
|
+
// bounded so stable worlds do not retain unbounded variants or raw secrets.
|
|
745
745
|
let ripgrepAvailabilityByBackend = new WeakMap<
|
|
746
746
|
t.LocalSpawn,
|
|
747
|
-
Map<string,
|
|
747
|
+
Map<string, ReturnType<typeof probeLocalCommandAvailability>>
|
|
748
748
|
>();
|
|
749
749
|
|
|
750
|
-
function envCacheKey(env: NodeJS.ProcessEnv | undefined): string {
|
|
751
|
-
// PATH is the only env entry that affects command lookup, but
|
|
752
|
-
// hashing the whole env keeps the key correct for hosts that
|
|
753
|
-
// vary anything else relevant. Stable JSON via sorted keys so
|
|
754
|
-
// {A:1,B:2} and {B:2,A:1} produce the same hash.
|
|
755
|
-
if (env == null) return '';
|
|
756
|
-
const sorted: Record<string, string | undefined> = {};
|
|
757
|
-
for (const k of Object.keys(env).sort()) {
|
|
758
|
-
sorted[k] = env[k];
|
|
759
|
-
}
|
|
760
|
-
return JSON.stringify(sorted);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
750
|
async function isRipgrepAvailable(
|
|
764
751
|
config: t.LocalExecutionConfig
|
|
765
752
|
): Promise<boolean> {
|
|
@@ -769,20 +756,23 @@ async function isRipgrepAvailable(
|
|
|
769
756
|
envMap = new Map();
|
|
770
757
|
ripgrepAvailabilityByBackend.set(backend, envMap);
|
|
771
758
|
}
|
|
772
|
-
const envKey =
|
|
759
|
+
const envKey = commandAvailabilityEnvCacheKey(config.env);
|
|
773
760
|
let probePromise = envMap.get(envKey);
|
|
774
761
|
if (probePromise == null) {
|
|
775
|
-
probePromise =
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
envMap.
|
|
762
|
+
probePromise = probeLocalCommandAvailability('rg', ['--version'], config);
|
|
763
|
+
setCommandAvailabilityCacheEntry(envMap, envKey, probePromise);
|
|
764
|
+
}
|
|
765
|
+
const result = await probePromise;
|
|
766
|
+
if (!result.cacheable && envMap.get(envKey) === probePromise) {
|
|
767
|
+
envMap.delete(envKey);
|
|
768
|
+
}
|
|
769
|
+
if (result.cacheUntil != null && result.cacheUntil <= Date.now()) {
|
|
770
|
+
if (envMap.get(envKey) === probePromise) {
|
|
771
|
+
envMap.delete(envKey);
|
|
772
|
+
}
|
|
773
|
+
return isRipgrepAvailable(config);
|
|
784
774
|
}
|
|
785
|
-
return
|
|
775
|
+
return result.available;
|
|
786
776
|
}
|
|
787
777
|
|
|
788
778
|
/**
|
|
@@ -21,6 +21,36 @@ const DEFAULT_MAX_OUTPUT_CHARS = 200000;
|
|
|
21
21
|
const DEFAULT_MAX_SPAWNED_BYTES = 50 * 1024 * 1024;
|
|
22
22
|
const DEFAULT_LOCAL_SESSION_ID = 'local';
|
|
23
23
|
const DEFAULT_SHELL = process.platform === 'win32' ? 'bash.exe' : 'bash';
|
|
24
|
+
const MAX_COMMAND_AVAILABILITY_ENVIRONMENTS = 16;
|
|
25
|
+
const NEGATIVE_COMMAND_AVAILABILITY_TTL_MS = 5000;
|
|
26
|
+
|
|
27
|
+
/** Produces a stable, non-plaintext key for environment-sensitive probes. */
|
|
28
|
+
export function commandAvailabilityEnvCacheKey(
|
|
29
|
+
env: NodeJS.ProcessEnv | undefined
|
|
30
|
+
): string {
|
|
31
|
+
if (env == null) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
const sorted = Object.keys(env)
|
|
35
|
+
.sort()
|
|
36
|
+
.map((key): [string, string | null] => [key, env[key] ?? null]);
|
|
37
|
+
return createHash('sha256').update(JSON.stringify(sorted)).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Adds a probe entry while bounding retained environment variants. */
|
|
41
|
+
export function setCommandAvailabilityCacheEntry<T>(
|
|
42
|
+
cache: Map<string, T>,
|
|
43
|
+
key: string,
|
|
44
|
+
value: T
|
|
45
|
+
): void {
|
|
46
|
+
if (!cache.has(key) && cache.size >= MAX_COMMAND_AVAILABILITY_ENVIRONMENTS) {
|
|
47
|
+
const oldestKey = cache.keys().next().value;
|
|
48
|
+
if (oldestKey !== undefined) {
|
|
49
|
+
cache.delete(oldestKey);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
cache.set(key, value);
|
|
53
|
+
}
|
|
24
54
|
|
|
25
55
|
// `(?:--\s+)?` before each destructive-target alternation: GNU/BSD
|
|
26
56
|
// utilities accept `--` as an end-of-options marker, so `rm -rf -- /`
|
|
@@ -259,13 +289,34 @@ export function getWorkspaceRoots(config?: t.LocalExecutionConfig): string[] {
|
|
|
259
289
|
return out;
|
|
260
290
|
}
|
|
261
291
|
|
|
292
|
+
/** Node-host execution world used when no backend override is configured. */
|
|
293
|
+
export const nodeExecutionWorld: t.ExecutionWorld = Object.freeze({
|
|
294
|
+
spawn: spawn as t.LocalSpawn,
|
|
295
|
+
fs: nodeWorkspaceFS,
|
|
296
|
+
sandboxed: false,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/** Resolves filesystem and subprocess capabilities as one execution world. */
|
|
300
|
+
export function getExecutionWorld(
|
|
301
|
+
config?: t.LocalExecutionConfig
|
|
302
|
+
): t.ExecutionWorld {
|
|
303
|
+
if (config?.exec == null && config?.spawn == null) {
|
|
304
|
+
return nodeExecutionWorld;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
spawn: config.exec?.spawn ?? config.spawn ?? nodeExecutionWorld.spawn,
|
|
308
|
+
fs: config.exec?.fs ?? nodeExecutionWorld.fs,
|
|
309
|
+
sandboxed: config.exec?.sandboxed ?? false,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
262
313
|
/**
|
|
263
314
|
* Pluggable spawn resolver. Honours `local.exec.spawn` first, falls
|
|
264
315
|
* back to the legacy top-level `local.spawn`, then to Node's
|
|
265
316
|
* `child_process.spawn`. Centralised so engine swapping is one knob.
|
|
266
317
|
*/
|
|
267
318
|
export function getSpawn(config?: t.LocalExecutionConfig): t.LocalSpawn {
|
|
268
|
-
return (config
|
|
319
|
+
return getExecutionWorld(config).spawn;
|
|
269
320
|
}
|
|
270
321
|
|
|
271
322
|
/**
|
|
@@ -274,7 +325,7 @@ export function getSpawn(config?: t.LocalExecutionConfig): t.LocalSpawn {
|
|
|
274
325
|
* its own implementation here and inherits every file-touching tool.
|
|
275
326
|
*/
|
|
276
327
|
export function getWorkspaceFS(config?: t.LocalExecutionConfig): WorkspaceFS {
|
|
277
|
-
return config
|
|
328
|
+
return getExecutionWorld(config).fs;
|
|
278
329
|
}
|
|
279
330
|
|
|
280
331
|
/**
|
|
@@ -345,7 +396,7 @@ function maybeWarnSandboxOff(config: t.LocalExecutionConfig): void {
|
|
|
345
396
|
if (
|
|
346
397
|
sandboxOffWarned ||
|
|
347
398
|
shouldUseLocalSandbox(config) ||
|
|
348
|
-
config.
|
|
399
|
+
getExecutionWorld(config).sandboxed
|
|
349
400
|
) {
|
|
350
401
|
return;
|
|
351
402
|
}
|
|
@@ -904,6 +955,72 @@ export async function spawnLocalProcess(
|
|
|
904
955
|
});
|
|
905
956
|
}
|
|
906
957
|
|
|
958
|
+
/** Result of a command-availability probe and whether its verdict is stable. */
|
|
959
|
+
export type CommandAvailabilityProbe = {
|
|
960
|
+
available: boolean;
|
|
961
|
+
cacheable: boolean;
|
|
962
|
+
cacheUntil?: number;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
async function isDurableCommandLookupError(
|
|
966
|
+
error: object,
|
|
967
|
+
config: t.LocalExecutionConfig
|
|
968
|
+
): Promise<boolean> {
|
|
969
|
+
if (!('code' in error) || error.code !== 'ENOENT') {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const cwd = getLocalCwd(config);
|
|
974
|
+
return (await getWorkspaceFS(config).stat(cwd)).isDirectory();
|
|
975
|
+
} catch {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Probes one executable without turning transient backend failures into
|
|
982
|
+
* permanent capability facts. Exit 126/127 is a definite unusable/missing
|
|
983
|
+
* executable. A native ENOENT is stable only when the current working
|
|
984
|
+
* directory exists; timeouts, transport failures, and ambiguous lookup
|
|
985
|
+
* failures are retried.
|
|
986
|
+
*/
|
|
987
|
+
export async function probeLocalCommandAvailability(
|
|
988
|
+
command: string,
|
|
989
|
+
args: string[],
|
|
990
|
+
config: t.LocalExecutionConfig
|
|
991
|
+
): Promise<CommandAvailabilityProbe> {
|
|
992
|
+
try {
|
|
993
|
+
const result = await spawnLocalProcess(
|
|
994
|
+
command,
|
|
995
|
+
args,
|
|
996
|
+
{ ...config, timeoutMs: 5000, sandbox: { enabled: false } },
|
|
997
|
+
{ internal: true }
|
|
998
|
+
);
|
|
999
|
+
const available = result.exitCode === 0;
|
|
1000
|
+
const durableNegative =
|
|
1001
|
+
result.exitCode === 126 || result.exitCode === 127;
|
|
1002
|
+
return {
|
|
1003
|
+
available,
|
|
1004
|
+
cacheable: available || durableNegative,
|
|
1005
|
+
...(durableNegative
|
|
1006
|
+
? { cacheUntil: Date.now() + NEGATIVE_COMMAND_AVAILABILITY_TTL_MS }
|
|
1007
|
+
: {}),
|
|
1008
|
+
};
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const cacheable =
|
|
1011
|
+
typeof error === 'object' &&
|
|
1012
|
+
error != null &&
|
|
1013
|
+
(await isDurableCommandLookupError(error, config));
|
|
1014
|
+
return {
|
|
1015
|
+
available: false,
|
|
1016
|
+
cacheable,
|
|
1017
|
+
...(cacheable
|
|
1018
|
+
? { cacheUntil: Date.now() + NEGATIVE_COMMAND_AVAILABILITY_TTL_MS }
|
|
1019
|
+
: {}),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
907
1024
|
export async function executeLocalBash(
|
|
908
1025
|
command: string,
|
|
909
1026
|
config: t.LocalExecutionConfig = {}
|