@agentgrader/agent-acp 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/dist/index.d.ts +13 -0
- package/dist/index.js +419 -0
- package/package.json +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AgentAdapter, SandboxHandle, AgentConfig, StepEvent, AgentResult } from '@agentgrader/core';
|
|
2
|
+
|
|
3
|
+
declare class AcpAgentAdapter implements AgentAdapter {
|
|
4
|
+
readonly name = "acp";
|
|
5
|
+
solve(input: {
|
|
6
|
+
prompt: string;
|
|
7
|
+
sandbox: SandboxHandle;
|
|
8
|
+
config: AgentConfig;
|
|
9
|
+
onStep: (step: StepEvent) => void;
|
|
10
|
+
}): Promise<AgentResult>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { AcpAgentAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { Writable, Readable } from 'stream';
|
|
4
|
+
import { ndJsonStream, ClientSideConnection, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk';
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
var DEFAULT_WORKSPACE_ROOT = "/app";
|
|
8
|
+
function resolveAcpSpawn(config) {
|
|
9
|
+
if (!config.acp_command) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"acp_command is required in agent config when using AcpAgentAdapter (e.g. acp_command: cursor-agent)."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
const explicitArgs = config.acp_args ?? [];
|
|
15
|
+
if (explicitArgs.length > 0) {
|
|
16
|
+
return { command: config.acp_command, args: explicitArgs };
|
|
17
|
+
}
|
|
18
|
+
const parts = config.acp_command.trim().split(/\s+/).filter(Boolean);
|
|
19
|
+
if (parts.length === 0) {
|
|
20
|
+
throw new Error("acp_command must not be empty.");
|
|
21
|
+
}
|
|
22
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
23
|
+
}
|
|
24
|
+
function resolveSandboxPath(path, workspaceRoot) {
|
|
25
|
+
if (path.startsWith("/")) {
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
return `${workspaceRoot}/${path}`.replace(/\/+/g, "/");
|
|
29
|
+
}
|
|
30
|
+
function shellQuote(value) {
|
|
31
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
32
|
+
}
|
|
33
|
+
function sliceFileContent(content, line, limit) {
|
|
34
|
+
if (line == null && limit == null) {
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
const lines = content.split("\n");
|
|
38
|
+
const start = Math.max((line ?? 1) - 1, 0);
|
|
39
|
+
const end = limit == null ? lines.length : start + limit;
|
|
40
|
+
return lines.slice(start, end).join("\n");
|
|
41
|
+
}
|
|
42
|
+
var SandboxAcpClient = class {
|
|
43
|
+
constructor(sandbox, workspaceRoot, onStep) {
|
|
44
|
+
this.sandbox = sandbox;
|
|
45
|
+
this.workspaceRoot = workspaceRoot;
|
|
46
|
+
this.onStep = onStep;
|
|
47
|
+
}
|
|
48
|
+
sandbox;
|
|
49
|
+
workspaceRoot;
|
|
50
|
+
onStep;
|
|
51
|
+
terminals = /* @__PURE__ */ new Map();
|
|
52
|
+
stepIndex = 0;
|
|
53
|
+
emitStep(partial) {
|
|
54
|
+
const step = {
|
|
55
|
+
index: this.stepIndex++,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
tokensIn: partial.tokensIn ?? 0,
|
|
58
|
+
tokensOut: partial.tokensOut ?? 0,
|
|
59
|
+
cachedTokens: partial.cachedTokens ?? 0,
|
|
60
|
+
costUsd: partial.costUsd ?? 0,
|
|
61
|
+
kind: partial.kind,
|
|
62
|
+
tool: partial.tool,
|
|
63
|
+
content: partial.content
|
|
64
|
+
};
|
|
65
|
+
this.onStep(step);
|
|
66
|
+
}
|
|
67
|
+
async requestPermission(params) {
|
|
68
|
+
const allowOption = params.options.find((option) => option.kind === "allow_once" || option.kind === "allow_always") ?? params.options[0];
|
|
69
|
+
if (!allowOption) {
|
|
70
|
+
return { outcome: { outcome: "cancelled" } };
|
|
71
|
+
}
|
|
72
|
+
this.emitStep({
|
|
73
|
+
kind: "tool_call",
|
|
74
|
+
tool: "requestPermission",
|
|
75
|
+
content: params.toolCall.title ?? params.toolCall.toolCallId
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
outcome: {
|
|
79
|
+
outcome: "selected",
|
|
80
|
+
optionId: allowOption.optionId
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async sessionUpdate(params) {
|
|
85
|
+
const update = params.update;
|
|
86
|
+
switch (update.sessionUpdate) {
|
|
87
|
+
case "agent_message_chunk":
|
|
88
|
+
this.emitStep({
|
|
89
|
+
kind: "message",
|
|
90
|
+
content: extractTextContent(update)
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
case "agent_thought_chunk":
|
|
94
|
+
this.emitStep({
|
|
95
|
+
kind: "thinking",
|
|
96
|
+
content: extractTextContent(update)
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
case "tool_call":
|
|
100
|
+
this.emitStep({
|
|
101
|
+
kind: "tool_call",
|
|
102
|
+
tool: update.title ?? update.toolCallId,
|
|
103
|
+
content: update.kind ?? String(update.status ?? "")
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
case "tool_call_update":
|
|
107
|
+
this.emitStep({
|
|
108
|
+
kind: "tool_result",
|
|
109
|
+
tool: update.title ?? update.toolCallId,
|
|
110
|
+
content: String(update.status ?? "")
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async readTextFile(params) {
|
|
116
|
+
const path = resolveSandboxPath(params.path, this.workspaceRoot);
|
|
117
|
+
this.emitStep({
|
|
118
|
+
kind: "tool_call",
|
|
119
|
+
tool: "fs/read_text_file",
|
|
120
|
+
content: path
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const raw = await this.sandbox.readFile(path);
|
|
124
|
+
const content = sliceFileContent(raw, params.line, params.limit);
|
|
125
|
+
this.emitStep({
|
|
126
|
+
kind: "tool_result",
|
|
127
|
+
tool: "fs/read_text_file",
|
|
128
|
+
content: `${content.length} bytes`
|
|
129
|
+
});
|
|
130
|
+
return { content };
|
|
131
|
+
} catch {
|
|
132
|
+
throw RequestError.resourceNotFound(path);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async writeTextFile(params) {
|
|
136
|
+
const path = resolveSandboxPath(params.path, this.workspaceRoot);
|
|
137
|
+
this.emitStep({
|
|
138
|
+
kind: "tool_call",
|
|
139
|
+
tool: "fs/write_text_file",
|
|
140
|
+
content: path
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
await this.sandbox.writeFile(path, params.content);
|
|
144
|
+
this.emitStep({
|
|
145
|
+
kind: "tool_result",
|
|
146
|
+
tool: "fs/write_text_file",
|
|
147
|
+
content: "ok"
|
|
148
|
+
});
|
|
149
|
+
return {};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
+
throw RequestError.internalError({ path }, message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async createTerminal(params) {
|
|
156
|
+
const terminalId = randomUUID();
|
|
157
|
+
const outputFile = `/tmp/acp-term-${terminalId}.out`;
|
|
158
|
+
const exitFile = `/tmp/acp-term-${terminalId}.exit`;
|
|
159
|
+
const pidFile = `/tmp/acp-term-${terminalId}.pid`;
|
|
160
|
+
const cwd = params.cwd ?? this.workspaceRoot;
|
|
161
|
+
const args = (params.args ?? []).map(shellQuote).join(" ");
|
|
162
|
+
const envPrefix = params.env && params.env.length > 0 ? `${params.env.map((entry) => `${entry.name}=${shellQuote(entry.value)}`).join(" ")} ` : "";
|
|
163
|
+
const command = `${envPrefix}${shellQuote(params.command)}${args ? ` ${args}` : ""}`;
|
|
164
|
+
const shellCmd = [
|
|
165
|
+
`cd ${shellQuote(cwd)}`,
|
|
166
|
+
`(${command}) > ${shellQuote(outputFile)} 2>&1; echo $? > ${shellQuote(exitFile)} & echo $! > ${shellQuote(pidFile)}`
|
|
167
|
+
].join(" && ");
|
|
168
|
+
this.emitStep({
|
|
169
|
+
kind: "tool_call",
|
|
170
|
+
tool: "terminal/create",
|
|
171
|
+
content: params.command
|
|
172
|
+
});
|
|
173
|
+
const spawnResult = await this.sandbox.exec(shellCmd);
|
|
174
|
+
if (spawnResult.exitCode !== 0) {
|
|
175
|
+
throw RequestError.internalError(
|
|
176
|
+
{ command: params.command, stderr: spawnResult.stderr },
|
|
177
|
+
`Failed to start terminal command: ${spawnResult.stderr || spawnResult.stdout}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
this.terminals.set(terminalId, {
|
|
181
|
+
outputFile,
|
|
182
|
+
exitFile,
|
|
183
|
+
pidFile,
|
|
184
|
+
released: false
|
|
185
|
+
});
|
|
186
|
+
this.emitStep({
|
|
187
|
+
kind: "tool_result",
|
|
188
|
+
tool: "terminal/create",
|
|
189
|
+
content: terminalId
|
|
190
|
+
});
|
|
191
|
+
return { terminalId };
|
|
192
|
+
}
|
|
193
|
+
async terminalOutput(params) {
|
|
194
|
+
const terminal = this.getTerminal(params.terminalId);
|
|
195
|
+
const output = await this.readOptionalFile(terminal.outputFile);
|
|
196
|
+
const exitStatus = await this.readExitStatus(terminal);
|
|
197
|
+
this.emitStep({
|
|
198
|
+
kind: "tool_call",
|
|
199
|
+
tool: "terminal/output",
|
|
200
|
+
content: params.terminalId
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
output,
|
|
204
|
+
truncated: false,
|
|
205
|
+
exitStatus
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async waitForTerminalExit(params) {
|
|
209
|
+
const terminal = this.getTerminal(params.terminalId);
|
|
210
|
+
const deadline = Date.now() + 18e4;
|
|
211
|
+
while (Date.now() < deadline) {
|
|
212
|
+
const exitStatus = await this.readExitStatus(terminal);
|
|
213
|
+
if (exitStatus) {
|
|
214
|
+
this.emitStep({
|
|
215
|
+
kind: "tool_result",
|
|
216
|
+
tool: "terminal/wait_for_exit",
|
|
217
|
+
content: String(exitStatus.exitCode ?? "signal")
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
exitCode: exitStatus.exitCode ?? null,
|
|
221
|
+
signal: exitStatus.signal ?? null
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
await sleep(100);
|
|
225
|
+
}
|
|
226
|
+
throw RequestError.internalError(
|
|
227
|
+
{ terminalId: params.terminalId },
|
|
228
|
+
"Timed out waiting for terminal command to exit."
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
async killTerminal(params) {
|
|
232
|
+
const terminal = this.getTerminal(params.terminalId);
|
|
233
|
+
const pid = (await this.readOptionalFile(terminal.pidFile)).trim();
|
|
234
|
+
if (pid) {
|
|
235
|
+
await this.sandbox.exec(`kill -TERM ${shellQuote(pid)} 2>/dev/null || kill -KILL ${shellQuote(pid)} 2>/dev/null || true`);
|
|
236
|
+
}
|
|
237
|
+
this.emitStep({
|
|
238
|
+
kind: "tool_call",
|
|
239
|
+
tool: "terminal/kill",
|
|
240
|
+
content: params.terminalId
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async releaseTerminal(params) {
|
|
244
|
+
const terminal = this.getTerminal(params.terminalId);
|
|
245
|
+
if (terminal.released) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const pid = (await this.readOptionalFile(terminal.pidFile)).trim();
|
|
249
|
+
if (pid) {
|
|
250
|
+
await this.sandbox.exec(`kill -TERM ${shellQuote(pid)} 2>/dev/null || true`);
|
|
251
|
+
}
|
|
252
|
+
terminal.released = true;
|
|
253
|
+
this.terminals.delete(params.terminalId);
|
|
254
|
+
await this.sandbox.exec(
|
|
255
|
+
`rm -f ${shellQuote(terminal.outputFile)} ${shellQuote(terminal.exitFile)} ${shellQuote(terminal.pidFile)}`
|
|
256
|
+
);
|
|
257
|
+
this.emitStep({
|
|
258
|
+
kind: "tool_result",
|
|
259
|
+
tool: "terminal/release",
|
|
260
|
+
content: params.terminalId
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
getTerminal(terminalId) {
|
|
264
|
+
const terminal = this.terminals.get(terminalId);
|
|
265
|
+
if (!terminal || terminal.released) {
|
|
266
|
+
throw RequestError.invalidParams({ terminalId }, `Unknown or released terminal: ${terminalId}`);
|
|
267
|
+
}
|
|
268
|
+
return terminal;
|
|
269
|
+
}
|
|
270
|
+
async readOptionalFile(path) {
|
|
271
|
+
try {
|
|
272
|
+
return await this.sandbox.readFile(path);
|
|
273
|
+
} catch {
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async readExitStatus(terminal) {
|
|
278
|
+
const exitRaw = (await this.readOptionalFile(terminal.exitFile)).trim();
|
|
279
|
+
if (!exitRaw) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const exitCode = Number.parseInt(exitRaw, 10);
|
|
283
|
+
return {
|
|
284
|
+
exitCode: Number.isNaN(exitCode) ? null : exitCode,
|
|
285
|
+
signal: null
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function extractTextContent(chunk) {
|
|
290
|
+
if (chunk.content.type === "text") {
|
|
291
|
+
return chunk.content.text;
|
|
292
|
+
}
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
function sleep(ms) {
|
|
296
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
297
|
+
}
|
|
298
|
+
function killProcess(proc) {
|
|
299
|
+
if (proc.exitCode != null || proc.signalCode != null) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
proc.kill("SIGTERM");
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
if (proc.exitCode == null && proc.signalCode == null) {
|
|
305
|
+
proc.kill("SIGKILL");
|
|
306
|
+
}
|
|
307
|
+
}, 2e3).unref();
|
|
308
|
+
}
|
|
309
|
+
var AcpAgentAdapter = class {
|
|
310
|
+
name = "acp";
|
|
311
|
+
async solve(input) {
|
|
312
|
+
const { prompt, sandbox, config, onStep } = input;
|
|
313
|
+
const { command, args } = resolveAcpSpawn(config);
|
|
314
|
+
const workspaceRoot = config.acp_cwd ?? DEFAULT_WORKSPACE_ROOT;
|
|
315
|
+
const client = new SandboxAcpClient(sandbox, workspaceRoot, onStep);
|
|
316
|
+
let proc;
|
|
317
|
+
let connection;
|
|
318
|
+
try {
|
|
319
|
+
proc = spawn(command, args, {
|
|
320
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
321
|
+
env: {
|
|
322
|
+
...process.env,
|
|
323
|
+
...config.acp_env ?? {}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
const stdin = proc.stdin;
|
|
327
|
+
const stdout = proc.stdout;
|
|
328
|
+
if (!stdin || !stdout) {
|
|
329
|
+
throw new Error(`Failed to open stdio pipes for ACP agent: ${command}`);
|
|
330
|
+
}
|
|
331
|
+
proc.on("error", (err) => {
|
|
332
|
+
onStep({
|
|
333
|
+
index: 0,
|
|
334
|
+
kind: "message",
|
|
335
|
+
timestamp: Date.now(),
|
|
336
|
+
tokensIn: 0,
|
|
337
|
+
tokensOut: 0,
|
|
338
|
+
cachedTokens: 0,
|
|
339
|
+
costUsd: 0,
|
|
340
|
+
content: `ACP subprocess error: ${err.message}`
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
const stream = ndJsonStream(
|
|
344
|
+
Writable.toWeb(stdin),
|
|
345
|
+
Readable.toWeb(stdout)
|
|
346
|
+
);
|
|
347
|
+
connection = new ClientSideConnection(() => client, stream);
|
|
348
|
+
await connection.initialize({
|
|
349
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
350
|
+
clientCapabilities: {
|
|
351
|
+
fs: {
|
|
352
|
+
readTextFile: true,
|
|
353
|
+
writeTextFile: true
|
|
354
|
+
},
|
|
355
|
+
terminal: true
|
|
356
|
+
},
|
|
357
|
+
clientInfo: {
|
|
358
|
+
name: "Agentgrader",
|
|
359
|
+
version: "1.0.0"
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
const session = await connection.newSession({
|
|
363
|
+
cwd: workspaceRoot,
|
|
364
|
+
mcpServers: []
|
|
365
|
+
});
|
|
366
|
+
const stepTimeoutMs = config.step_timeout_ms ?? 12e4;
|
|
367
|
+
let timedOut = false;
|
|
368
|
+
let promptResponse;
|
|
369
|
+
try {
|
|
370
|
+
promptResponse = await Promise.race([
|
|
371
|
+
connection.prompt({
|
|
372
|
+
sessionId: session.sessionId,
|
|
373
|
+
prompt: [{ type: "text", text: prompt }]
|
|
374
|
+
}),
|
|
375
|
+
new Promise((_, reject) => {
|
|
376
|
+
setTimeout(() => {
|
|
377
|
+
timedOut = true;
|
|
378
|
+
void connection?.cancel({ sessionId: session.sessionId }).catch(() => void 0);
|
|
379
|
+
reject(new Error(`ACP prompt timed out after ${stepTimeoutMs}ms (step_timeout_ms).`));
|
|
380
|
+
}, stepTimeoutMs);
|
|
381
|
+
})
|
|
382
|
+
]);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (timedOut) {
|
|
385
|
+
const finalDiff2 = await sandbox.gitDiff().catch(() => "");
|
|
386
|
+
return {
|
|
387
|
+
finished: false,
|
|
388
|
+
finalDiff: finalDiff2,
|
|
389
|
+
error: err instanceof Error ? err.message : `ACP prompt timed out after ${stepTimeoutMs}ms (step_timeout_ms).`
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
const finalDiff = await sandbox.gitDiff();
|
|
395
|
+
const finished = promptResponse.stopReason === "end_turn";
|
|
396
|
+
return {
|
|
397
|
+
finished,
|
|
398
|
+
finalDiff,
|
|
399
|
+
error: finished ? void 0 : `ACP agent stopped with reason: ${promptResponse.stopReason}`
|
|
400
|
+
};
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const finalDiff = await sandbox.gitDiff().catch(() => "");
|
|
403
|
+
return {
|
|
404
|
+
finished: false,
|
|
405
|
+
finalDiff,
|
|
406
|
+
error: err instanceof Error ? err.message : String(err)
|
|
407
|
+
};
|
|
408
|
+
} finally {
|
|
409
|
+
if (connection) {
|
|
410
|
+
await Promise.race([connection.closed, sleep(2e3)]);
|
|
411
|
+
}
|
|
412
|
+
if (proc) {
|
|
413
|
+
killProcess(proc);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export { AcpAgentAdapter };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentgrader/agent-acp",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Agent Client Protocol (ACP) adapter for the Agentgrader framework",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm --dts --clean --treeshake",
|
|
21
|
+
"build:watch": "tsup src/index.ts --format esm --dts --watch"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@agentclientprotocol/sdk": "^0.25.1",
|
|
25
|
+
"@agentgrader/core": "^1.3.0",
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.5.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@agentgrader/core": "^1.3.0"
|
|
33
|
+
}
|
|
34
|
+
}
|