@bluecopa/harness 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -117
- package/dist/arc/index.d.ts +796 -0
- package/dist/arc/index.js +2863 -0
- package/dist/arc/index.js.map +1 -0
- package/dist/observability/otel.d.ts +36 -0
- package/dist/observability/otel.js +73 -0
- package/dist/observability/otel.js.map +1 -0
- package/dist/shared-types-DRxnerLT.d.ts +138 -0
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +282 -0
- package/dist/skills/index.js.map +1 -0
- package/package.json +26 -2
- package/AGENTS.md +0 -18
- package/docs/guides/observability.md +0 -32
- package/docs/guides/providers.md +0 -51
- package/docs/guides/skills.md +0 -25
- package/docs/security/skill-sandbox-threat-model.md +0 -20
- package/src/agent/create-agent.ts +0 -884
- package/src/agent/create-tools.ts +0 -33
- package/src/agent/step-executor.ts +0 -15
- package/src/agent/types.ts +0 -57
- package/src/context/llm-compaction-strategy.ts +0 -37
- package/src/context/prepare-step.ts +0 -65
- package/src/context/token-tracker.ts +0 -26
- package/src/extracted/manifest.json +0 -10
- package/src/extracted/prompts/compaction.md +0 -5
- package/src/extracted/prompts/system.md +0 -5
- package/src/extracted/tools.json +0 -82
- package/src/hooks/hook-runner.ts +0 -22
- package/src/hooks/tool-wrappers.ts +0 -64
- package/src/interfaces/compaction-strategy.ts +0 -18
- package/src/interfaces/hooks.ts +0 -24
- package/src/interfaces/sandbox-provider.ts +0 -29
- package/src/interfaces/session-store.ts +0 -48
- package/src/interfaces/tool-provider.ts +0 -70
- package/src/loop/bridge.ts +0 -363
- package/src/loop/context-store.ts +0 -207
- package/src/loop/lcm-tool-loop.ts +0 -163
- package/src/loop/vercel-agent-loop.ts +0 -279
- package/src/observability/context.ts +0 -17
- package/src/observability/metrics.ts +0 -27
- package/src/observability/otel.ts +0 -105
- package/src/observability/tracing.ts +0 -13
- package/src/optimization/agent-evaluator.ts +0 -40
- package/src/optimization/config-serializer.ts +0 -16
- package/src/optimization/optimization-runner.ts +0 -39
- package/src/optimization/trace-collector.ts +0 -33
- package/src/permissions/permission-manager.ts +0 -34
- package/src/providers/composite-tool-provider.ts +0 -72
- package/src/providers/control-plane-e2b-executor.ts +0 -218
- package/src/providers/e2b-tool-provider.ts +0 -68
- package/src/providers/local-tool-provider.ts +0 -190
- package/src/providers/skill-sandbox-provider.ts +0 -46
- package/src/sessions/file-session-store.ts +0 -61
- package/src/sessions/in-memory-session-store.ts +0 -39
- package/src/sessions/session-manager.ts +0 -44
- package/src/skills/skill-loader.ts +0 -52
- package/src/skills/skill-manager.ts +0 -175
- package/src/skills/skill-router.ts +0 -99
- package/src/skills/skill-types.ts +0 -26
- package/src/subagents/subagent-manager.ts +0 -22
- package/src/subagents/task-tool.ts +0 -13
- package/tests/integration/agent-loop-basic.spec.ts +0 -56
- package/tests/integration/agent-skill-default-from-sandbox.spec.ts +0 -66
- package/tests/integration/concurrency-single-turn.spec.ts +0 -35
- package/tests/integration/otel-metrics-emission.spec.ts +0 -62
- package/tests/integration/otel-trace-propagation.spec.ts +0 -48
- package/tests/integration/parity-benchmark.spec.ts +0 -45
- package/tests/integration/provider-local-smoke.spec.ts +0 -63
- package/tests/integration/session-resume.spec.ts +0 -30
- package/tests/integration/skill-install-rollback.spec.ts +0 -64
- package/tests/integration/skill-sandbox-file-blob.spec.ts +0 -54
- package/tests/integration/skills-progressive-disclosure.spec.ts +0 -61
- package/tests/integration/streaming-compaction-boundary.spec.ts +0 -43
- package/tests/integration/structured-messages-agent.spec.ts +0 -265
- package/tests/integration/subagent-isolation.spec.ts +0 -24
- package/tests/security/skill-sandbox-isolation.spec.ts +0 -51
- package/tests/unit/create-tools-schema-parity.spec.ts +0 -22
- package/tests/unit/extracted-manifest.spec.ts +0 -41
- package/tests/unit/interfaces-contract.spec.ts +0 -101
- package/tests/unit/structured-messages.spec.ts +0 -176
- package/tests/unit/token-tracker.spec.ts +0 -22
- package/tsconfig.json +0 -14
- package/vitest.config.ts +0 -7
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { AgentEvaluator, type EvaluationTask } from './agent-evaluator';
|
|
2
|
-
import type { AgentRunResult } from '../agent/types';
|
|
3
|
-
|
|
4
|
-
export interface Candidate {
|
|
5
|
-
id: string;
|
|
6
|
-
run: (prompt: string) => Promise<AgentRunResult>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface OptimizationResult {
|
|
10
|
-
bestCandidateId: string;
|
|
11
|
-
bestScore: number;
|
|
12
|
-
scores: Array<{ candidateId: string; averageScore: number }>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class OptimizationRunner {
|
|
16
|
-
async run(candidates: Candidate[], tasks: EvaluationTask[]): Promise<OptimizationResult> {
|
|
17
|
-
if (candidates.length === 0) {
|
|
18
|
-
throw new Error('at least one candidate is required');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const scores: Array<{ candidateId: string; averageScore: number }> = [];
|
|
22
|
-
|
|
23
|
-
for (const candidate of candidates) {
|
|
24
|
-
const evaluator = new AgentEvaluator(candidate.run);
|
|
25
|
-
const results = await Promise.all(tasks.map((task) => evaluator.evaluate(task)));
|
|
26
|
-
const averageScore = results.reduce((sum, item) => sum + item.score, 0) / tasks.length;
|
|
27
|
-
scores.push({ candidateId: candidate.id, averageScore });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const sorted = [...scores].sort((a, b) => b.averageScore - a.averageScore);
|
|
31
|
-
const best = sorted[0]!;
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
bestCandidateId: best.candidateId,
|
|
35
|
-
bestScore: best.averageScore,
|
|
36
|
-
scores
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export interface TraceEvent {
|
|
2
|
-
type: string;
|
|
3
|
-
payload: Record<string, unknown>;
|
|
4
|
-
timestamp: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface ExecutionTrace {
|
|
8
|
-
runId: string;
|
|
9
|
-
events: TraceEvent[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class TraceCollector {
|
|
13
|
-
private readonly trace: ExecutionTrace;
|
|
14
|
-
|
|
15
|
-
constructor(runId: string) {
|
|
16
|
-
this.trace = { runId, events: [] };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
add(type: string, payload: Record<string, unknown> = {}): void {
|
|
20
|
-
this.trace.events.push({
|
|
21
|
-
type,
|
|
22
|
-
payload,
|
|
23
|
-
timestamp: Date.now()
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
toExecutionTrace(): ExecutionTrace {
|
|
28
|
-
return {
|
|
29
|
-
runId: this.trace.runId,
|
|
30
|
-
events: [...this.trace.events]
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export type PermissionMode = 'allow_all' | 'deny_all' | 'ask';
|
|
2
|
-
|
|
3
|
-
export interface PermissionRequest {
|
|
4
|
-
toolName: string;
|
|
5
|
-
input?: Record<string, unknown>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type PermissionResolver = (request: PermissionRequest) => Promise<boolean>;
|
|
9
|
-
|
|
10
|
-
export class PermissionManager {
|
|
11
|
-
constructor(
|
|
12
|
-
private readonly mode: PermissionMode,
|
|
13
|
-
private readonly resolver?: PermissionResolver
|
|
14
|
-
) {}
|
|
15
|
-
|
|
16
|
-
async check(request: PermissionRequest): Promise<{ allow: boolean; reason?: string }> {
|
|
17
|
-
if (this.mode === 'allow_all') {
|
|
18
|
-
return { allow: true };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (this.mode === 'deny_all') {
|
|
22
|
-
return { allow: false, reason: `Tool ${request.toolName} denied by policy` };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!this.resolver) {
|
|
26
|
-
return { allow: false, reason: 'Permission resolver is missing for ask mode' };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const allow = await this.resolver(request);
|
|
30
|
-
return allow
|
|
31
|
-
? { allow: true }
|
|
32
|
-
: { allow: false, reason: `Tool ${request.toolName} denied by resolver` };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BashOptions,
|
|
3
|
-
GlobOptions,
|
|
4
|
-
GrepOptions,
|
|
5
|
-
ReadOptions,
|
|
6
|
-
ToolProvider,
|
|
7
|
-
ToolProviderCapabilities,
|
|
8
|
-
ToolResult
|
|
9
|
-
} from '../interfaces/tool-provider';
|
|
10
|
-
|
|
11
|
-
export class CompositeToolProvider implements ToolProvider {
|
|
12
|
-
constructor(private readonly providers: ToolProvider[]) {}
|
|
13
|
-
|
|
14
|
-
private pick(capability: keyof ToolProviderCapabilities): ToolProvider {
|
|
15
|
-
const provider = this.providers.find((candidate) => candidate.capabilities()[capability]);
|
|
16
|
-
if (!provider) {
|
|
17
|
-
throw new Error(`no provider with capability: ${capability}`);
|
|
18
|
-
}
|
|
19
|
-
return provider;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
capabilities(): ToolProviderCapabilities {
|
|
23
|
-
return {
|
|
24
|
-
bash: this.providers.some((p) => p.capabilities().bash),
|
|
25
|
-
fileSystem: this.providers.some((p) => p.capabilities().fileSystem),
|
|
26
|
-
webFetch: this.providers.some((p) => p.capabilities().webFetch),
|
|
27
|
-
webSearch: this.providers.some((p) => p.capabilities().webSearch),
|
|
28
|
-
codeExecution: this.providers.some((p) => p.capabilities().codeExecution),
|
|
29
|
-
sandboxed: this.providers.every((p) => p.capabilities().sandboxed)
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
bash(command: string, options?: BashOptions): Promise<ToolResult> {
|
|
34
|
-
return this.pick('bash').bash(command, options);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
readFile(path: string, options?: ReadOptions): Promise<ToolResult> {
|
|
38
|
-
return this.pick('fileSystem').readFile(path, options);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
writeFile(path: string, content: string): Promise<ToolResult> {
|
|
42
|
-
return this.pick('fileSystem').writeFile(path, content);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
editFile(path: string, oldText: string, newText: string): Promise<ToolResult> {
|
|
46
|
-
return this.pick('fileSystem').editFile(path, oldText, newText);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
glob(pattern: string, options?: GlobOptions): Promise<ToolResult> {
|
|
50
|
-
return this.pick('fileSystem').glob(pattern, options);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
grep(pattern: string, path?: string, options?: GrepOptions): Promise<ToolResult> {
|
|
54
|
-
return this.pick('fileSystem').grep(pattern, path, options);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
webFetch(options: { url: string; selector?: string | undefined; maxContentLength?: number | undefined; headers?: Record<string, string> | undefined }): Promise<ToolResult> {
|
|
58
|
-
const provider = this.providers.find((p) => p.capabilities().webFetch && p.webFetch);
|
|
59
|
-
if (!provider?.webFetch) {
|
|
60
|
-
return Promise.resolve({ success: false, output: '', error: 'webFetch unavailable' });
|
|
61
|
-
}
|
|
62
|
-
return provider.webFetch(options);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
webSearch(query: string): Promise<ToolResult> {
|
|
66
|
-
const provider = this.providers.find((p) => p.capabilities().webSearch && p.webSearch);
|
|
67
|
-
if (!provider?.webSearch) {
|
|
68
|
-
return Promise.resolve({ success: false, output: '', error: 'webSearch unavailable' });
|
|
69
|
-
}
|
|
70
|
-
return provider.webSearch(query);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BatchOp,
|
|
3
|
-
BatchResult,
|
|
4
|
-
BashOptions,
|
|
5
|
-
GlobOptions,
|
|
6
|
-
GrepOptions,
|
|
7
|
-
ReadOptions,
|
|
8
|
-
ToolResult
|
|
9
|
-
} from '../interfaces/tool-provider';
|
|
10
|
-
import type { E2BExecutor } from './e2b-tool-provider';
|
|
11
|
-
|
|
12
|
-
type CreateSandboxResponse = {
|
|
13
|
-
sandboxId: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type CommandResponse = {
|
|
17
|
-
exitCode: number | null;
|
|
18
|
-
stdout: string;
|
|
19
|
-
stderr: string;
|
|
20
|
-
status: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type ReadFileResponse = {
|
|
24
|
-
content: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type ControlPlaneExecutorConfig = {
|
|
28
|
-
baseUrl: string;
|
|
29
|
-
apiKey: string;
|
|
30
|
-
templateId?: string;
|
|
31
|
-
cpu?: number;
|
|
32
|
-
memoryMb?: number;
|
|
33
|
-
ttlSeconds?: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export class ControlPlaneE2BExecutor implements E2BExecutor {
|
|
37
|
-
private sandboxId: string | null = null;
|
|
38
|
-
|
|
39
|
-
constructor(private readonly config: ControlPlaneExecutorConfig) {}
|
|
40
|
-
|
|
41
|
-
static fromEnv(): ControlPlaneE2BExecutor {
|
|
42
|
-
const baseUrl = process.env.SAMYX_BASE_URL ?? process.env.SANDBOX_BASE_URL;
|
|
43
|
-
const apiKey = process.env.SAMYX_API_KEY ?? process.env.SANDBOX_API_KEY;
|
|
44
|
-
if (!baseUrl || !apiKey) {
|
|
45
|
-
throw new Error('Missing SAMYX_BASE_URL/SAMYX_API_KEY (or SANDBOX_BASE_URL/SANDBOX_API_KEY) for default provider');
|
|
46
|
-
}
|
|
47
|
-
return new ControlPlaneE2BExecutor({
|
|
48
|
-
baseUrl,
|
|
49
|
-
apiKey,
|
|
50
|
-
templateId: process.env.SANDBOX_TEMPLATE ?? 'polyglot-v1'
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
get activeSandboxId(): string | null {
|
|
55
|
-
return this.sandboxId;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async initialize(): Promise<void> {
|
|
59
|
-
if (this.sandboxId) return;
|
|
60
|
-
const body = {
|
|
61
|
-
templateID: this.config.templateId ?? 'polyglot-v1',
|
|
62
|
-
cpu: this.config.cpu ?? 1,
|
|
63
|
-
memoryMb: this.config.memoryMb ?? 1024,
|
|
64
|
-
timeout: this.config.ttlSeconds ?? 3600
|
|
65
|
-
};
|
|
66
|
-
const sandbox = await this.request<CreateSandboxResponse>('/sandboxes', {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
body: JSON.stringify(body)
|
|
69
|
-
});
|
|
70
|
-
this.sandboxId = sandbox.sandboxId;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async destroy(): Promise<void> {
|
|
74
|
-
if (!this.sandboxId) return;
|
|
75
|
-
await this.requestRaw(`/sandboxes/${this.sandboxId}`, { method: 'DELETE' }, true);
|
|
76
|
-
this.sandboxId = null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async bash(command: string, options?: BashOptions): Promise<ToolResult> {
|
|
80
|
-
await this.initialize();
|
|
81
|
-
const sandboxId = this.requireSandboxId();
|
|
82
|
-
const response = await this.request<CommandResponse>(`/sandboxes/${sandboxId}/commands`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
body: JSON.stringify({
|
|
85
|
-
command,
|
|
86
|
-
cwd: options?.cwd,
|
|
87
|
-
timeoutMs: options?.timeout ?? 60_000
|
|
88
|
-
})
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const success = response.exitCode === 0;
|
|
92
|
-
return {
|
|
93
|
-
success,
|
|
94
|
-
output: response.stdout ?? '',
|
|
95
|
-
error: success ? undefined : response.stderr || `command failed: ${response.status}`,
|
|
96
|
-
metadata: { sandboxId, status: response.status, exitCode: response.exitCode }
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async readFile(filePath: string, _options?: ReadOptions): Promise<ToolResult> {
|
|
101
|
-
await this.initialize();
|
|
102
|
-
const sandboxId = this.requireSandboxId();
|
|
103
|
-
try {
|
|
104
|
-
const resp = await this.request<ReadFileResponse>(
|
|
105
|
-
`/sandboxes/${sandboxId}/files/content?path=${encodeURIComponent(filePath)}`
|
|
106
|
-
);
|
|
107
|
-
return { success: true, output: resp.content, metadata: { sandboxId, path: filePath } };
|
|
108
|
-
} catch (err: any) {
|
|
109
|
-
return { success: false, output: '', error: err.message, metadata: { sandboxId, path: filePath } };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async readFileBytes(filePath: string): Promise<Uint8Array> {
|
|
114
|
-
await this.initialize();
|
|
115
|
-
const sandboxId = this.requireSandboxId();
|
|
116
|
-
const resp = await this.requestRaw(
|
|
117
|
-
`/sandboxes/${sandboxId}/files/raw?path=${encodeURIComponent(filePath)}`
|
|
118
|
-
);
|
|
119
|
-
return new Uint8Array(await resp.arrayBuffer());
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async writeFileBytes(filePath: string, data: Uint8Array): Promise<ToolResult> {
|
|
123
|
-
await this.initialize();
|
|
124
|
-
const sandboxId = this.requireSandboxId();
|
|
125
|
-
const b64 = Buffer.from(data).toString('base64');
|
|
126
|
-
try {
|
|
127
|
-
await this.request(`/sandboxes/${sandboxId}/files/content`, {
|
|
128
|
-
method: 'PUT',
|
|
129
|
-
body: JSON.stringify({ path: filePath, content: b64, encoding: 'base64' })
|
|
130
|
-
});
|
|
131
|
-
return { success: true, output: 'ok', metadata: { sandboxId, path: filePath } };
|
|
132
|
-
} catch (err: any) {
|
|
133
|
-
return { success: false, output: '', error: err.message, metadata: { sandboxId, path: filePath } };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async writeFile(filePath: string, content: string): Promise<ToolResult> {
|
|
138
|
-
await this.initialize();
|
|
139
|
-
const sandboxId = this.requireSandboxId();
|
|
140
|
-
const b64 = Buffer.from(content, 'utf8').toString('base64');
|
|
141
|
-
try {
|
|
142
|
-
await this.request(`/sandboxes/${sandboxId}/files/content`, {
|
|
143
|
-
method: 'PUT',
|
|
144
|
-
body: JSON.stringify({ path: filePath, content: b64, encoding: 'base64' })
|
|
145
|
-
});
|
|
146
|
-
return { success: true, output: 'ok', metadata: { sandboxId, path: filePath } };
|
|
147
|
-
} catch (err: any) {
|
|
148
|
-
return { success: false, output: '', error: err.message, metadata: { sandboxId, path: filePath } };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async editFile(filePath: string, oldText: string, newText: string): Promise<ToolResult> {
|
|
153
|
-
const current = await this.readFile(filePath);
|
|
154
|
-
if (!current.success) return current;
|
|
155
|
-
if (!current.output.includes(oldText)) {
|
|
156
|
-
return { success: false, output: '', error: 'old text not found', metadata: { path: filePath } };
|
|
157
|
-
}
|
|
158
|
-
return this.writeFile(filePath, current.output.replace(oldText, newText));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async glob(pattern: string, _options?: GlobOptions): Promise<ToolResult> {
|
|
162
|
-
// Parse glob: extract search directory and filename pattern
|
|
163
|
-
// e.g. "/home/user/**/*.py" -> dir="/home/user", namePattern="*.py"
|
|
164
|
-
const lastSlash = pattern.lastIndexOf('/');
|
|
165
|
-
let dir = '/';
|
|
166
|
-
let namePattern = pattern;
|
|
167
|
-
if (lastSlash >= 0) {
|
|
168
|
-
dir = pattern.slice(0, lastSlash) || '/';
|
|
169
|
-
namePattern = pattern.slice(lastSlash + 1);
|
|
170
|
-
}
|
|
171
|
-
// Strip leading glob segments from dir (e.g. /home/user/**/foo -> /home/user)
|
|
172
|
-
dir = dir.replace(/\/\*\*.*$/, '').replace(/\/\*$/, '') || '/';
|
|
173
|
-
const escapedDir = dir.replace(/'/g, "'\\''");
|
|
174
|
-
const escapedName = namePattern.replace(/'/g, "'\\''");
|
|
175
|
-
return this.bash(`find '${escapedDir}' -type f -name '${escapedName}' 2>/dev/null | head -n 200`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async grep(pattern: string, grepPath?: string, _options?: GrepOptions): Promise<ToolResult> {
|
|
179
|
-
const escapedPattern = pattern.replace(/'/g, "'\\''");
|
|
180
|
-
const escapedPath = (grepPath ?? '/').replace(/'/g, "'\\''");
|
|
181
|
-
return this.bash(`grep -R -n -- '${escapedPattern}' '${escapedPath}' 2>/dev/null | head -n 200`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async batch(ops: BatchOp[]): Promise<BatchResult[]> {
|
|
185
|
-
await this.initialize();
|
|
186
|
-
const sandboxId = this.requireSandboxId();
|
|
187
|
-
return this.request<BatchResult[]>(`/sandboxes/${sandboxId}/batch`, {
|
|
188
|
-
method: 'POST',
|
|
189
|
-
body: JSON.stringify({ ops })
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private requireSandboxId(): string {
|
|
194
|
-
if (!this.sandboxId) throw new Error('sandbox is not initialized');
|
|
195
|
-
return this.sandboxId;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private async request<T = unknown>(path: string, init: RequestInit = {}): Promise<T> {
|
|
199
|
-
const response = await this.requestRaw(path, init);
|
|
200
|
-
return (await response.json()) as T;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private async requestRaw(path: string, init: RequestInit = {}, tolerate404 = false): Promise<Response> {
|
|
204
|
-
const headers: Record<string, string> = {
|
|
205
|
-
'x-api-key': this.config.apiKey,
|
|
206
|
-
...(init.headers as Record<string, string> | undefined)
|
|
207
|
-
};
|
|
208
|
-
if (init.body !== undefined && !headers['content-type']) {
|
|
209
|
-
headers['content-type'] = 'application/json';
|
|
210
|
-
}
|
|
211
|
-
const response = await fetch(`${this.config.baseUrl}${path}`, { ...init, headers });
|
|
212
|
-
if (!response.ok && !(tolerate404 && response.status === 404)) {
|
|
213
|
-
const text = await response.text();
|
|
214
|
-
throw new Error(`${init.method ?? 'GET'} ${path} failed: ${response.status} ${text}`);
|
|
215
|
-
}
|
|
216
|
-
return response;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BatchOp,
|
|
3
|
-
BatchResult,
|
|
4
|
-
BashOptions,
|
|
5
|
-
GlobOptions,
|
|
6
|
-
GrepOptions,
|
|
7
|
-
ReadOptions,
|
|
8
|
-
ToolProvider,
|
|
9
|
-
ToolProviderCapabilities,
|
|
10
|
-
ToolResult
|
|
11
|
-
} from '../interfaces/tool-provider';
|
|
12
|
-
|
|
13
|
-
export interface E2BExecutor {
|
|
14
|
-
bash(command: string, options?: BashOptions): Promise<ToolResult>;
|
|
15
|
-
readFile(path: string, options?: ReadOptions): Promise<ToolResult>;
|
|
16
|
-
writeFile(path: string, content: string): Promise<ToolResult>;
|
|
17
|
-
editFile(path: string, oldText: string, newText: string): Promise<ToolResult>;
|
|
18
|
-
glob(pattern: string, options?: GlobOptions): Promise<ToolResult>;
|
|
19
|
-
grep(pattern: string, path?: string, options?: GrepOptions): Promise<ToolResult>;
|
|
20
|
-
batch?: (ops: BatchOp[]) => Promise<BatchResult[]>;
|
|
21
|
-
readFileBytes?(path: string): Promise<Uint8Array>;
|
|
22
|
-
writeFileBytes?(path: string, data: Uint8Array): Promise<ToolResult>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class E2BToolProvider implements ToolProvider {
|
|
26
|
-
constructor(private readonly executor: E2BExecutor) {}
|
|
27
|
-
|
|
28
|
-
capabilities(): ToolProviderCapabilities {
|
|
29
|
-
return {
|
|
30
|
-
bash: true,
|
|
31
|
-
fileSystem: true,
|
|
32
|
-
webFetch: false,
|
|
33
|
-
webSearch: false,
|
|
34
|
-
codeExecution: true,
|
|
35
|
-
sandboxed: true
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
bash(command: string, options?: BashOptions): Promise<ToolResult> {
|
|
40
|
-
return this.executor.bash(command, options);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
readFile(path: string, options?: ReadOptions): Promise<ToolResult> {
|
|
44
|
-
return this.executor.readFile(path, options);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
writeFile(path: string, content: string): Promise<ToolResult> {
|
|
48
|
-
return this.executor.writeFile(path, content);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
editFile(path: string, oldText: string, newText: string): Promise<ToolResult> {
|
|
52
|
-
return this.executor.editFile(path, oldText, newText);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
glob(pattern: string, options?: GlobOptions): Promise<ToolResult> {
|
|
56
|
-
return this.executor.glob(pattern, options);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
grep(pattern: string, path?: string, options?: GrepOptions): Promise<ToolResult> {
|
|
60
|
-
return this.executor.grep(pattern, path, options);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
get batch(): ((ops: BatchOp[]) => Promise<BatchResult[]>) | undefined {
|
|
64
|
-
return this.executor.batch
|
|
65
|
-
? (ops: BatchOp[]) => this.executor.batch!(ops)
|
|
66
|
-
: undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { exec as execCallback } from 'node:child_process';
|
|
2
|
-
import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
BashOptions,
|
|
8
|
-
GlobOptions,
|
|
9
|
-
GrepOptions,
|
|
10
|
-
ReadOptions,
|
|
11
|
-
ToolProvider,
|
|
12
|
-
ToolProviderCapabilities,
|
|
13
|
-
ToolResult
|
|
14
|
-
} from '../interfaces/tool-provider';
|
|
15
|
-
|
|
16
|
-
const exec = promisify(execCallback);
|
|
17
|
-
|
|
18
|
-
function regexFromGlob(pattern: string): RegExp {
|
|
19
|
-
const escaped = pattern
|
|
20
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
21
|
-
.replace(/\*\*/g, '::DOUBLESTAR::')
|
|
22
|
-
.replace(/\*/g, '[^/]*')
|
|
23
|
-
.replace(/::DOUBLESTAR::/g, '.*')
|
|
24
|
-
.replace(/\?/g, '.');
|
|
25
|
-
return new RegExp(`^${escaped}$`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function walkFiles(baseDir: string): Promise<string[]> {
|
|
29
|
-
const out: string[] = [];
|
|
30
|
-
|
|
31
|
-
async function walk(current: string): Promise<void> {
|
|
32
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
33
|
-
for (const entry of entries) {
|
|
34
|
-
const fullPath = join(current, entry.name);
|
|
35
|
-
if (entry.isDirectory()) {
|
|
36
|
-
await walk(fullPath);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
out.push(fullPath);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await walk(baseDir);
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class LocalToolProvider implements ToolProvider {
|
|
48
|
-
constructor(private readonly baseDir: string = process.cwd()) {}
|
|
49
|
-
|
|
50
|
-
private resolvePath(path: string): string {
|
|
51
|
-
return resolve(this.baseDir, path);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
capabilities(): ToolProviderCapabilities {
|
|
55
|
-
return {
|
|
56
|
-
bash: true,
|
|
57
|
-
fileSystem: true,
|
|
58
|
-
webFetch: false,
|
|
59
|
-
webSearch: false,
|
|
60
|
-
codeExecution: true,
|
|
61
|
-
sandboxed: false
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async bash(command: string, options?: BashOptions): Promise<ToolResult> {
|
|
66
|
-
try {
|
|
67
|
-
const { stdout, stderr } = await exec(command, {
|
|
68
|
-
cwd: options?.cwd ?? this.baseDir,
|
|
69
|
-
timeout: options?.timeout ?? 30000,
|
|
70
|
-
maxBuffer: 10 * 1024 * 1024
|
|
71
|
-
});
|
|
72
|
-
return {
|
|
73
|
-
success: true,
|
|
74
|
-
output: [stdout, stderr].filter(Boolean).join('')
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
const message = error instanceof Error ? error.message : 'bash command failed';
|
|
78
|
-
return { success: false, output: '', error: message };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async readFile(path: string, options?: ReadOptions): Promise<ToolResult> {
|
|
83
|
-
try {
|
|
84
|
-
const content = await readFile(this.resolvePath(path), 'utf8');
|
|
85
|
-
const lines = content.split('\n');
|
|
86
|
-
|
|
87
|
-
let selected = lines;
|
|
88
|
-
if (options?.lineRange) {
|
|
89
|
-
const [start, end] = options.lineRange;
|
|
90
|
-
selected = lines.slice(Math.max(0, start - 1), end);
|
|
91
|
-
}
|
|
92
|
-
if (options?.maxLines) {
|
|
93
|
-
selected = selected.slice(0, options.maxLines);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { success: true, output: selected.join('\n') };
|
|
97
|
-
} catch (error) {
|
|
98
|
-
const message = error instanceof Error ? error.message : 'read failed';
|
|
99
|
-
return { success: false, output: '', error: message };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async writeFile(path: string, content: string): Promise<ToolResult> {
|
|
104
|
-
try {
|
|
105
|
-
const resolved = this.resolvePath(path);
|
|
106
|
-
await mkdir(dirname(resolved), { recursive: true });
|
|
107
|
-
await writeFile(resolved, content, 'utf8');
|
|
108
|
-
return { success: true, output: 'ok' };
|
|
109
|
-
} catch (error) {
|
|
110
|
-
const message = error instanceof Error ? error.message : 'write failed';
|
|
111
|
-
return { success: false, output: '', error: message };
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async editFile(path: string, oldText: string, newText: string): Promise<ToolResult> {
|
|
116
|
-
const readResult = await this.readFile(path);
|
|
117
|
-
if (!readResult.success) {
|
|
118
|
-
return readResult;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!readResult.output.includes(oldText)) {
|
|
122
|
-
return { success: false, output: '', error: 'old_text not found' };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return this.writeFile(path, readResult.output.replace(oldText, newText));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async glob(pattern: string, options?: GlobOptions): Promise<ToolResult> {
|
|
129
|
-
try {
|
|
130
|
-
const matcher = regexFromGlob(pattern);
|
|
131
|
-
const ignore = new Set(options?.ignore ?? []);
|
|
132
|
-
const files = await walkFiles(this.baseDir);
|
|
133
|
-
|
|
134
|
-
const results = files
|
|
135
|
-
.map((file) => relative(this.baseDir, file))
|
|
136
|
-
.filter((file) => {
|
|
137
|
-
for (const skip of ignore) {
|
|
138
|
-
if (file.startsWith(skip)) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return matcher.test(file);
|
|
143
|
-
})
|
|
144
|
-
.slice(0, options?.maxResults ?? 1000);
|
|
145
|
-
|
|
146
|
-
return { success: true, output: results.join('\n') };
|
|
147
|
-
} catch (error) {
|
|
148
|
-
const message = error instanceof Error ? error.message : 'glob failed';
|
|
149
|
-
return { success: false, output: '', error: message };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async grep(pattern: string, path = '.', options?: GrepOptions): Promise<ToolResult> {
|
|
154
|
-
try {
|
|
155
|
-
const target = this.resolvePath(path);
|
|
156
|
-
const targetStat = await stat(target);
|
|
157
|
-
const files = targetStat.isDirectory() ? await walkFiles(target) : [target];
|
|
158
|
-
|
|
159
|
-
const flags = options?.caseInsensitive ? 'i' : '';
|
|
160
|
-
const sourcePattern = options?.wholeWord ? `\\b${pattern}\\b` : pattern;
|
|
161
|
-
const matcher = new RegExp(sourcePattern, flags);
|
|
162
|
-
|
|
163
|
-
const maxResults = options?.maxResults ?? 200;
|
|
164
|
-
const matches: string[] = [];
|
|
165
|
-
|
|
166
|
-
for (const file of files) {
|
|
167
|
-
const rel = relative(this.baseDir, file);
|
|
168
|
-
if (options?.include && !regexFromGlob(options.include).test(rel)) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const content = await readFile(file, 'utf8');
|
|
173
|
-
const lines = content.split('\n');
|
|
174
|
-
lines.forEach((line, idx) => {
|
|
175
|
-
if (matches.length >= maxResults) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (matcher.test(line)) {
|
|
179
|
-
matches.push(`${rel}:${idx + 1}:${line}`);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { success: true, output: matches.join('\n') };
|
|
185
|
-
} catch (error) {
|
|
186
|
-
const message = error instanceof Error ? error.message : 'grep failed';
|
|
187
|
-
return { success: false, output: '', error: message };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|