diff --git a/.changeset/fresh-selfmod-package.md b/.changeset/fresh-selfmod-package.md new file mode 100644 index 000000000..a725d8c1f --- /dev/null +++ b/.changeset/fresh-selfmod-package.md @@ -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. diff --git a/.changeset/source-workspace-exports.md b/.changeset/source-workspace-exports.md new file mode 100644 index 000000000..abac54c56 --- /dev/null +++ b/.changeset/source-workspace-exports.md @@ -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. diff --git a/packages/eve-selfmod/.gitignore b/packages/eve-selfmod/.gitignore new file mode 100644 index 000000000..828bc4f17 --- /dev/null +++ b/packages/eve-selfmod/.gitignore @@ -0,0 +1,3 @@ +dist +scaffold +*.tsbuildinfo diff --git a/packages/eve-selfmod/README.md b/packages/eve-selfmod/README.md new file mode 100644 index 000000000..e3094d22e --- /dev/null +++ b/packages/eve-selfmod/README.md @@ -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. diff --git a/packages/eve-selfmod/extension/extension.ts b/packages/eve-selfmod/extension/extension.ts new file mode 100644 index 000000000..eb45471fd --- /dev/null +++ b/packages/eve-selfmod/extension/extension.ts @@ -0,0 +1,3 @@ +import { defineExtension } from "eve/extension"; + +export default defineExtension(); diff --git a/packages/eve-selfmod/extension/instructions.ts b/packages/eve-selfmod/extension/instructions.ts new file mode 100644 index 000000000..23e0f8917 --- /dev/null +++ b/packages/eve-selfmod/extension/instructions.ts @@ -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//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.`, +}); diff --git a/packages/eve-selfmod/package.json b/packages/eve-selfmod/package.json new file mode 100644 index 000000000..ee7bab4a3 --- /dev/null +++ b/packages/eve-selfmod/package.json @@ -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" + } + } +} diff --git a/packages/eve-selfmod/src/agent.ts b/packages/eve-selfmod/src/agent.ts new file mode 100644 index 000000000..5ea67e82e --- /dev/null +++ b/packages/eve-selfmod/src/agent.ts @@ -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(); diff --git a/packages/eve-selfmod/src/index.test.ts b/packages/eve-selfmod/src/index.test.ts new file mode 100644 index 000000000..568036644 --- /dev/null +++ b/packages/eve-selfmod/src/index.test.ts @@ -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" }); + }); +}); diff --git a/packages/eve-selfmod/src/index.ts b/packages/eve-selfmod/src/index.ts new file mode 100644 index 000000000..640b9284b --- /dev/null +++ b/packages/eve-selfmod/src/index.ts @@ -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, + }; +} diff --git a/packages/eve-selfmod/src/sandbox.ts b/packages/eve-selfmod/src/sandbox.ts new file mode 100644 index 000000000..dbe4cec6b --- /dev/null +++ b/packages/eve-selfmod/src/sandbox.ts @@ -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", + }, + ], + }); + }, + }), +}); diff --git a/packages/eve-selfmod/tsconfig.json b/packages/eve-selfmod/tsconfig.json new file mode 100644 index 000000000..914d8f409 --- /dev/null +++ b/packages/eve-selfmod/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["extension/**/*.ts", "src/**/*.ts"] +} diff --git a/packages/eve-selfmod/tsconfig.scaffold.json b/packages/eve-selfmod/tsconfig.scaffold.json new file mode 100644 index 000000000..6a7377f81 --- /dev/null +++ b/packages/eve-selfmod/tsconfig.scaffold.json @@ -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"] +} diff --git a/packages/eve/src/internal/authored-module-loader.scenario.test.ts b/packages/eve/src/internal/authored-module-loader.scenario.test.ts index ad3993797..cdb0f5964 100644 --- a/packages/eve/src/internal/authored-module-loader.scenario.test.ts +++ b/packages/eve/src/internal/authored-module-loader.scenario.test.ts @@ -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", diff --git a/packages/eve/src/internal/authored-module-loader.ts b/packages/eve/src/internal/authored-module-loader.ts index e90b0fb7e..c9e87d5e4 100644 --- a/packages/eve/src/internal/authored-module-loader.ts +++ b/packages/eve/src/internal/authored-module-loader.ts @@ -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), @@ -415,6 +416,7 @@ async function buildAuthoredModuleBundle( platform: "node", plugins, resolve: { + conditionNames: ["eve-source", "node", "import", "default"], extensions: [...RESOLVE_EXTENSIONS], }, tsconfig: tsconfigPath, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0eef5572..61f9d4b45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1424,6 +1424,25 @@ importers: specifier: 'catalog:' version: 4.1.10(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.1.5(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + packages/eve-selfmod: + dependencies: + just-bash: + specifier: 3.1.0 + version: 3.1.0(supports-color@10.2.2) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 25.9.1 + eve: + specifier: workspace:* + version: link:../eve + typescript: + specifier: 'catalog:' + version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.10(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.1.5(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + packages: '@agent-browser/eve@0.33.0': diff --git a/scripts/assert-changeset-publish-packages.mjs b/scripts/assert-changeset-publish-packages.mjs index 1fc514644..be3932f05 100644 --- a/scripts/assert-changeset-publish-packages.mjs +++ b/scripts/assert-changeset-publish-packages.mjs @@ -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) {