Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-selfmod-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve-selfmod": patch
---

Publish a configurable, development-only source editing subagent kit that mounts authored agent source read-write and provides its instructions through a mounted extension. The registry scaffold uses one `defineSelfmod` configuration for the subagent and sandbox.
5 changes: 5 additions & 0 deletions .changeset/source-workspace-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Honor `eve-source` package export conditions when bundling authored modules so linked workspace capability packages work before their distribution build exists.
3 changes: 3 additions & 0 deletions packages/eve-selfmod/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
scaffold
*.tsbuildinfo
42 changes: 42 additions & 0 deletions packages/eve-selfmod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# eve-selfmod

A development-only source editing subagent for [eve](https://eve.dev).

Install the package and scaffold a declared subagent from the eve registry:

```sh
eve add experimental/selfmod
```

The command creates one selfmod configuration and thin filesystem entrypoints:

```text
agent/subagents/selfmod/
├── config.ts
├── agent.ts
├── sandbox.ts
└── extensions/
└── selfmod.ts
```

```ts
// agent/subagents/selfmod/config.ts
import { defineSelfmod } from "eve-selfmod/config";

export const selfmod = defineSelfmod({
model: "anthropic/claude-sonnet-5",
});

// agent/subagents/selfmod/agent.ts
import { selfmod } from "./config";
export default selfmod.agent;

// agent/subagents/selfmod/sandbox.ts
import { selfmod } from "./config";
export default selfmod.sandbox;

// agent/subagents/selfmod/extensions/selfmod.ts
export { default } from "eve-selfmod";
```

`defineSelfmod` returns the coordinated subagent, sandbox, and extension definitions. Set `model` in the shared configuration to choose the selfmod subagent's model; it defaults to `anthropic/claude-sonnet-5`. The extension entrypoint remains a direct package mount so eve can discover its extension contributions statically. With no configuration it preserves the development-only behavior: the sandbox mounts the application's authored `agent/` directory read-write at `/source`, where the subagent can inspect and edit it with eve's default bash and filesystem tools.
3 changes: 3 additions & 0 deletions packages/eve-selfmod/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineExtension } from "eve/extension";

export default defineExtension();
9 changes: 9 additions & 0 deletions packages/eve-selfmod/extension/instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineInstructions } from "eve/instructions";

export default defineInstructions({
markdown: `You are a source editor for an eve application.

The live authored agent directory is mounted read-write at /source. Use /workspace only for temporary files. Inspect /source with bash, glob, grep, and read_file instead of guessing filenames. Batch independent discovery, searches, and file reads into as few tool calls as possible; prefer one shell call containing multiple independent commands over separate bash, grep, sed, or cat calls for each path. Runtime skill paths under $HOME/.agents and /workspace/skills belong to the caller's sandbox, are not authored source, and may not exist here. Never search those runtime paths even when the caller mentions them; discover the corresponding authored definition under /source instead. /source is the mounted authored agent directory, not the application or package root. Do not look for package.json, src/, or other project-level files. Do not assume conventional filenames such as tools/index.ts or skills/<name>/SKILL.md. eve definitions are filesystem-first and may be flat files. Read the exact discovered paths before editing them.

Make requested source changes directly with bash or write_file. Before overwriting an existing file with write_file, call read_file on that exact path; reading it through bash, cat, grep, or sed does not satisfy the overwrite guard. Keep changes minimal and modify only what the developer requested. “Minimal” means the smallest change that actually implements the request, not necessarily the smallest diff. Do not substitute prompting for implementation when the requested behavior requires deterministic logic or an external side effect. You cannot access application files outside the authored agent directory or run host binaries such as git, node, pnpm, or tsc. Source changes do not affect the current turn; after a successful development rebuild, they take effect on the next turn. Return a concise summary of files changed and any validation limitations.`,
});
87 changes: 87 additions & 0 deletions packages/eve-selfmod/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"name": "eve-selfmod",
"version": "0.0.0",
"description": "Development-only source editing subagent for eve applications.",
"keywords": [
"agents",
"eve",
"extension",
"self-modifying"
],
"homepage": "https://github.com/vercel/eve#readme",
"bugs": {
"url": "https://github.com/vercel/eve/issues"
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/vercel/eve.git",
"directory": "packages/eve-selfmod"
},
"files": [
"dist",
"scaffold",
"README.md"
],
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./config": {
"types": "./scaffold/index.d.ts",
"default": "./scaffold/index.js"
},
"./agent": {
"eve-source": "./src/agent.ts",
"types": "./scaffold/agent.d.ts",
"import": "./scaffold/agent.js",
"default": "./scaffold/agent.js"
},
"./sandbox": {
"eve-source": "./src/sandbox.ts",
"types": "./scaffold/sandbox.d.ts",
"import": "./scaffold/sandbox.js",
"default": "./scaffold/sandbox.js"
},
"./extension": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./tools": {
"types": "./dist/tools/index.d.ts",
"default": "./dist/tools/index.mjs"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "eve extension build && tsc -p tsconfig.scaffold.json",
"prepare": "pnpm run build",
"test:unit": "vitest run src",
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.scaffold.json --noEmit"
},
"dependencies": {
"just-bash": "3.1.0"
},
"devDependencies": {
"@types/node": "catalog:",
"eve": "workspace:*",
"typescript": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"eve": "workspace:*"
},
"engines": {
"node": ">=24"
},
"eve": {
"extension": {
"source": "./extension",
"dist": "./dist/extension"
}
}
}
30 changes: 30 additions & 0 deletions packages/eve-selfmod/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineAgent, defineDynamic, type AgentModelDefinition } from "eve";

/** Default model used by selfmod when `defineSelfmod` does not override it. */
export const DEFAULT_SELFMOD_MODEL = "anthropic/claude-sonnet-5";

/** Creates selfmod's development-only dynamic subagent definition. */
export function createSelfmodAgent(model: AgentModelDefinition = DEFAULT_SELFMOD_MODEL) {
return defineDynamic({
build: { externalDependencies: ["eve-selfmod"] },
events: {
"session.started": () =>
process.env.EVE_DEV === "1"
? defineAgent({
description:
"Delegate here when the developer asks to change this eve agent or its authored source. " +
"Describe the requested behavior change without guessing source paths or passing runtime skill paths under $HOME/.agents or /workspace/skills; the subagent discovers authored files under its /source mount. " +
"Treat requests for persistent changes to future behavior or capabilities as source-modification requests, even when the developer does not mention files or source code. " +
"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. " +
"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.",
model,
})
: null,
},
});
}

export default createSelfmodAgent();
36 changes: 36 additions & 0 deletions packages/eve-selfmod/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { afterEach, describe, expect, it } from "vitest";

import { DEFAULT_SELFMOD_MODEL } from "./agent.js";
import { defineSelfmod } from "./index.js";

const originalEveDev = process.env.EVE_DEV;

afterEach(() => {
if (originalEveDev === undefined) {
delete process.env.EVE_DEV;
} else {
process.env.EVE_DEV = originalEveDev;
}
});

describe("defineSelfmod", () => {
it("returns the coordinated definitions with the default model", async () => {
process.env.EVE_DEV = "1";

const selfmod = defineSelfmod();
const definition = await selfmod.agent.events["session.started"]?.({}, {} as never);

expect(definition).toMatchObject({ model: DEFAULT_SELFMOD_MODEL });
expect(selfmod.extension).toBeDefined();
expect(selfmod.sandbox).toBeDefined();
});

it("configures the subagent model", async () => {
process.env.EVE_DEV = "1";

const selfmod = defineSelfmod({ model: "openai/gpt-5" });
const definition = await selfmod.agent.events["session.started"]?.({}, {} as never);

expect(definition).toMatchObject({ model: "openai/gpt-5" });
});
});
35 changes: 35 additions & 0 deletions packages/eve-selfmod/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createRequire } from "node:module";

import type { AgentModelDefinition } from "eve";
import type { NoConfigExtensionHandle } from "eve/extension";

import type { createSelfmodAgent } from "./agent.js";
import type sandboxDefinition from "./sandbox.js";

const require = createRequire(import.meta.url);

/** Configuration for the selfmod subagent kit. */
export interface SelfmodOptions {
/**
* Model used by the selfmod subagent.
*
* @default "anthropic/claude-sonnet-5"
*/
readonly model?: AgentModelDefinition;
}

/**
* Defines the coordinated subagent, sandbox, and extension declarations for
* selfmod. With no options, selfmod is available only during local development.
*/
export function defineSelfmod(options: SelfmodOptions = {}) {
const agentModule = require("eve-selfmod/agent") as {
readonly createSelfmodAgent: typeof createSelfmodAgent;
};

return {
agent: agentModule.createSelfmodAgent(options.model),
extension: require("eve-selfmod/extension").default as NoConfigExtensionHandle,
sandbox: require("eve-selfmod/sandbox").default as typeof sandboxDefinition,
};
}
26 changes: 26 additions & 0 deletions packages/eve-selfmod/src/sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { resolve } from "node:path";

import { defineSandbox } from "eve/sandbox";
import { justbash } from "eve/sandbox/just-bash";
import { MountableFs, ReadWriteFs } from "just-bash";

export default defineSandbox({
backend: justbash({
async filesystem({ appRoot, defaultFilesystem }) {
await defaultFilesystem.mkdir("/source", { recursive: true });
return new MountableFs({
base: defaultFilesystem,
mounts: [
{
filesystem: new ReadWriteFs({
allowSymlinks: false,
maxFileReadSize: Number.MAX_SAFE_INTEGER,
root: resolve(appRoot, "agent"),
}),
mountPoint: "/source",
},
],
});
},
}),
});
9 changes: 9 additions & 0 deletions packages/eve-selfmod/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"rootDir": "."
},
"include": ["extension/**/*.ts", "src/**/*.ts"]
}
13 changes: 13 additions & 0 deletions packages/eve-selfmod/tsconfig.scaffold.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"noEmit": false,
"outDir": "./scaffold",
"rootDir": "./src"
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ describe("loadAuthoredModuleNamespace", () => {
JSON.stringify(
{
exports: {
"./exa-linkedin": "./src/exa-linkedin.ts",
"./exa-linkedin": {
"eve-source": "./src/exa-linkedin.ts",
default: "./dist/exa-linkedin.js",
},
},
name: "@repo/enrichment",
type: "module",
Expand Down
2 changes: 2 additions & 0 deletions packages/eve/src/internal/authored-module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export async function bundleAuthoredModuleMapForGeneration(input: {
platform: "node",
plugins,
resolve: {
conditionNames: ["eve-source", "node", "import", "default"],
extensions: [...RESOLVE_EXTENSIONS],
},
tsconfig: resolveAuthoredTsConfigPath(packageRoot),
Expand Down Expand Up @@ -415,6 +416,7 @@ async function buildAuthoredModuleBundle(
platform: "node",
plugins,
resolve: {
conditionNames: ["eve-source", "node", "import", "default"],
extensions: [...RESOLVE_EXTENSIONS],
},
tsconfig: tsconfigPath,
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/assert-changeset-publish-packages.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { access, readdir, readFile } from "node:fs/promises";
import { join } from "node:path";

const allowedPublicPackages = new Set(["eve"]);
const allowedPublicPackages = new Set(["eve", "eve-selfmod"]);
const workspaceRoots = ["apps", "packages"];

async function readJson(path) {
Expand Down