mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
feat(selfmod): add local traces to selfmod sandbox (#2912)
Signed-off-by: prha <prha@vercel.com>
This commit is contained in:
@@ -19,7 +19,12 @@ describe("defineSelfModificationAgent", () => {
|
||||
const agent = defineSelfModificationAgent();
|
||||
const definition = await agent.events["session.started"]?.({}, {} as never);
|
||||
|
||||
expect(definition).toMatchObject({ model: DEFAULT_SELF_MODIFICATION_MODEL });
|
||||
expect(definition).toMatchObject({
|
||||
description: expect.stringContaining(
|
||||
"investigate, diagnose, or optimize the agent's behavior from local traces",
|
||||
),
|
||||
model: DEFAULT_SELF_MODIFICATION_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
it("configures the subagent model", async () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ export function defineSelfModificationAgent(options: SelfModificationAgentOption
|
||||
"Infer persistence from the request and conversation rather than waiting for phrases such as “modify your source.” " +
|
||||
"For example, asking the agent to stop always doing something, add a capability, or change future responses calls for inspecting and editing the authored source instead of providing a one-turn workaround. " +
|
||||
"Also delegate questions about which integrations, channels, connections, or capabilities are available to add: the subagent searches the eve registry and reports exact item addresses instead of guessing them. " +
|
||||
"Delegate requests to investigate, diagnose, or optimize the agent's behavior from local traces to this subagent: it can inspect the invoking session's trace and make persistent source changes when warranted. " +
|
||||
"Resolve short follow-ups such as “yes” or “do it” against the preceding conversation. " +
|
||||
"Source edits do not affect the caller’s current turn. After this subagent reports changes, do not invoke edited tools or attempt runtime verification until a new user turn. " +
|
||||
"If whether the requested change should persist is genuinely ambiguous, ask one concise clarifying question.",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { defineInstructions } from "eve/instructions";
|
||||
import { defineDynamic, defineInstructions } from "eve/instructions";
|
||||
|
||||
export default defineInstructions({
|
||||
markdown: `You are an expert coding assistant operating inside of an eve agent. You help users by reading files, editing code, and writing new files that shape the behavior of the agent itself.
|
||||
const instructions = `You are an expert coding assistant operating inside of an eve agent. You help users by reading files, editing code, and writing new files that shape the behavior of the agent itself.
|
||||
|
||||
The source code of the eve agent is mounted read-write at /source. Use bash only for read-only discovery and available validation commands. Never modify source files with bash, sed, awk, redirection, or scripting. /source is the authored agent directory.
|
||||
|
||||
@@ -23,5 +22,36 @@ The eve framework documentation is mounted read-only at /eve-docs. Read the eve
|
||||
|
||||
You cannot access application files outside the authored agent directory or run host binaries such as git, node, pnpm, or tsc.
|
||||
|
||||
Treat a successful file-edit tool result as confirmation; do not reread a file solely to verify that the edit succeeded. Do not approximate unavailable build or test commands with broad source searches.`,
|
||||
Treat a successful file-edit tool result as confirmation; do not reread a file solely to verify that the edit succeeded. Do not approximate unavailable build or test commands with broad source searches.
|
||||
|
||||
Local trace segments are mounted read-only at /traces when available. Inspect other traces only when the user asks about another session or broader behavior.`;
|
||||
|
||||
function readTrace(
|
||||
event: unknown,
|
||||
): { readonly traceFlags: number; readonly traceId: string } | undefined {
|
||||
return (
|
||||
event as {
|
||||
readonly data?: {
|
||||
readonly trace?: { readonly traceFlags: number; readonly traceId: string };
|
||||
};
|
||||
}
|
||||
).data?.trace;
|
||||
}
|
||||
|
||||
export default defineDynamic({
|
||||
events: {
|
||||
"session.started": (event) => {
|
||||
const trace = readTrace(event);
|
||||
if (trace === undefined) return defineInstructions({ markdown: instructions });
|
||||
|
||||
const availability =
|
||||
(trace.traceFlags & 1) === 1
|
||||
? "If local segments were captured,"
|
||||
: "This trace was not sampled, so local segments may be absent. If any are present,";
|
||||
|
||||
return defineInstructions({
|
||||
markdown: `${instructions}\n\nThe invoking trace has ID ${trace.traceId}. ${availability} inspect them at /traces/${trace.traceId}.`,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { IFileSystem } from "just-bash";
|
||||
@@ -8,7 +9,11 @@ export async function createSelfModificationFilesystem(input: {
|
||||
readonly justBash: typeof import("just-bash");
|
||||
}): Promise<IFileSystem> {
|
||||
const { MountableFs, OverlayFs, ReadWriteFs } = input.justBash;
|
||||
await input.defaultFilesystem.mkdir("/source", { recursive: true });
|
||||
const traceRoot = resolve(input.appRoot, ".eve/traces/v1");
|
||||
await Promise.all([
|
||||
input.defaultFilesystem.mkdir("/source", { recursive: true }),
|
||||
mkdir(traceRoot, { recursive: true }),
|
||||
]);
|
||||
return new MountableFs({
|
||||
base: input.defaultFilesystem,
|
||||
mounts: [
|
||||
@@ -20,6 +25,14 @@ export async function createSelfModificationFilesystem(input: {
|
||||
}),
|
||||
mountPoint: "/source",
|
||||
},
|
||||
{
|
||||
filesystem: new OverlayFs({
|
||||
mountPoint: "/",
|
||||
readOnly: true,
|
||||
root: traceRoot,
|
||||
}),
|
||||
mountPoint: "/traces",
|
||||
},
|
||||
{
|
||||
filesystem: new OverlayFs({
|
||||
mountPoint: "/",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import instructions from "./extension/instructions.js";
|
||||
|
||||
describe("self-modification instructions", () => {
|
||||
const ctx = {
|
||||
channel: {},
|
||||
messages: [],
|
||||
session: { auth: { current: null, initiator: null }, id: "selfmod" },
|
||||
};
|
||||
|
||||
it("points at the trace that invoked selfmod", () => {
|
||||
const traceId = "a".repeat(32);
|
||||
const definition = instructions.events["session.started"]?.(
|
||||
{
|
||||
data: { trace: { spanId: "b".repeat(16), traceFlags: 1, traceId } },
|
||||
type: "session.started",
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(definition).toMatchObject({
|
||||
markdown: expect.stringContaining(`invoking trace has ID ${traceId}`),
|
||||
});
|
||||
expect(definition).toMatchObject({ markdown: expect.stringContaining(`/traces/${traceId}`) });
|
||||
});
|
||||
|
||||
it("does not claim that trace coordinates guarantee local segments", () => {
|
||||
const traceId = "a".repeat(32);
|
||||
const definition = instructions.events["session.started"]?.(
|
||||
{
|
||||
data: { trace: { spanId: "b".repeat(16), traceFlags: 0, traceId } },
|
||||
type: "session.started",
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(definition).toMatchObject({
|
||||
markdown: expect.stringContaining("not sampled, so local segments may be absent"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,17 +17,21 @@ afterEach(async () => {
|
||||
);
|
||||
});
|
||||
|
||||
async function createAppRoot(): Promise<string> {
|
||||
async function createAppRoot(options: { traces?: boolean } = {}): Promise<string> {
|
||||
const appRoot = await mkdtemp(join(tmpdir(), "eve-self-modification-sandbox-"));
|
||||
temporaryDirectories.push(appRoot);
|
||||
await mkdir(join(appRoot, "agent"), { recursive: true });
|
||||
await mkdir(join(appRoot, "node_modules/eve/docs"), { recursive: true });
|
||||
if (options.traces !== false) {
|
||||
await mkdir(join(appRoot, ".eve/traces/v1/trace-1/segments"), { recursive: true });
|
||||
await writeFile(join(appRoot, ".eve/traces/v1/trace-1/segments/span.otlp.json"), "trace\n");
|
||||
}
|
||||
await writeFile(join(appRoot, "node_modules/eve/docs/README.md"), "installed eve docs\n");
|
||||
return appRoot;
|
||||
}
|
||||
|
||||
describe("self-modification filesystem", () => {
|
||||
it("mounts authored source read-write and installed eve docs read-only", async () => {
|
||||
it("mounts authored source read-write and traces and eve docs read-only", async () => {
|
||||
const appRoot = await createAppRoot();
|
||||
const filesystem = await createSelfModificationFilesystem({
|
||||
appRoot,
|
||||
@@ -35,6 +39,11 @@ describe("self-modification filesystem", () => {
|
||||
justBash,
|
||||
});
|
||||
|
||||
expect(await filesystem.readFile("/traces/trace-1/segments/span.otlp.json")).toBe("trace\n");
|
||||
await expect(
|
||||
filesystem.writeFile("/traces/trace-1/segments/span.otlp.json", "changed\n"),
|
||||
).rejects.toThrow(/read-only file system/u);
|
||||
|
||||
expect(await filesystem.readFile("/eve-docs/README.md")).toBe("installed eve docs\n");
|
||||
await expect(filesystem.writeFile("/eve-docs/README.md", "changed\n")).rejects.toThrow(
|
||||
/read-only file system/u,
|
||||
@@ -43,4 +52,15 @@ describe("self-modification filesystem", () => {
|
||||
await filesystem.writeFile("/source/instructions.md", "authored\n");
|
||||
expect(await readFile(join(appRoot, "agent/instructions.md"), "utf8")).toBe("authored\n");
|
||||
});
|
||||
|
||||
it("mounts an empty trace directory when no local traces have been captured", async () => {
|
||||
const appRoot = await createAppRoot({ traces: false });
|
||||
const filesystem = await createSelfModificationFilesystem({
|
||||
appRoot,
|
||||
defaultFilesystem: new justBash.InMemoryFs(),
|
||||
justBash,
|
||||
});
|
||||
|
||||
expect(await filesystem.readdir("/traces")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user