Skip to content
Merged
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ The mechanics are a single `agents.yaml`, a `validate → plan → apply` workfl
## Quick start

```bash
agents project init # create a directory project (or convert agents.yaml)
agents project init # create a managed-agent/ subdirectory
cd managed-agent
agents project validate # validate JSON, Markdown, skills, and local files
agents project build --dry-run # preview organization and generated YAML
agents project build -y # freeze the current source into a Build
agents project build # freeze the current source into a Build (no confirmation)
agents project publish -y # publish exactly that Build and record a version
agents project workbench # edit and debug the same directory project
```
Expand All @@ -89,6 +90,8 @@ Directory projects keep global settings in `project.json`, each Agent under `age

Fresh Init includes Skill, File, Vault, and Environment examples under each resource directory's `_examples/`, with bilingual configuration instructions. They are not linked in `agent.json`, do not enter generated YAML, and are not published remotely. Copy an example outside `_examples/` and configure its Agent reference to enable it.

Init defaults to `./managed-agent`. Use `agents project init --project .` to initialize in place or convert an existing `agents.yaml`; other project commands still default to the current working directory.

Workbench and CLI share `agents project version status|enable|disable|list|preview|restore`. Versions are Git-independent full source-tree snapshots: immutable manifests point to content-addressed text and binary blobs, while `.openagentpack/state.json` is always excluded. Restore writes a historical tree forward into the working directory without moving version history or remote State. Deployment and Channel declarations remain read-only in Workbench but participate in full project Publish.

The original YAML workflow remains available through `agents init`, `validate`, `plan`, `apply`, and `destroy`. `agents playground -f agents.yaml` continues to open a YAML Agent Session Preview, but YAML Apply no longer creates project versions and cannot be used inside a directory-project root.
Expand Down
28 changes: 23 additions & 5 deletions apps/server/tests/project-declarations.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test } from "bun:test";
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
Expand Down Expand Up @@ -217,18 +217,36 @@ describe("directory project declaration editing", () => {
expect(await readFile(statePath, "utf8")).toBe('{"remote":"latest"}\n');
});

test("removes a File declaration without deleting its local source", async () => {
test("protects an automatically associated Agent-local File even without an authored mount", async () => {
const { directory, manager } = await projectFixture({ fileMount: false });
const listed = await listProjectDeclarations(manager);
expect(resource(listed.resources, "file", "input").references.map((reference) => reference.path)).toEqual([
"agents.assistant.files",
]);
const preview = await previewDeclarationChange(
{ type: "file", id: "input", baseRevision: listed.revision, action: "delete" },
manager,
);
expect(preview.can_commit).toBe(false);
expect((await stat(join(directory, "agents/assistant/files/input/file.json"))).isFile()).toBe(true);
});

test("removes an unreferenced shared File declaration without deleting its local source", async () => {
const { directory, manager } = await projectFixture({ fileMount: false });
await mkdir(join(directory, "resources/files"), { recursive: true });
const fileDirectory = join(directory, "resources/files/input");
await rename(join(directory, "agents/assistant/files/input"), fileDirectory);
await manager.refreshAfterSourceMutation();
const listed = await listProjectDeclarations(manager);
await commitDeclarationChange(
{ type: "file", id: "input", baseRevision: listed.revision, action: "delete" },
manager,
);

expect(await stat(join(directory, "agents/assistant/files/input/file.json")).catch(() => null)).toBeNull();
expect(await readFile(join(directory, "agents/assistant/files/input/input.txt"), "utf8")).toBe("Keep local file\n");
expect(await stat(join(fileDirectory, "file.json")).catch(() => null)).toBeNull();
expect(await readFile(join(fileDirectory, "input.txt"), "utf8")).toBe("Keep local file\n");
expect(
await stat(join(directory, "agents/assistant/files/input", FILE_AUTO_ASSOCIATION_IGNORE_FILE)).then(
await stat(join(fileDirectory, FILE_AUTO_ASSOCIATION_IGNORE_FILE)).then(
() => true,
() => false,
),
Expand Down
23 changes: 23 additions & 0 deletions apps/server/tests/project-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ describe("ProjectRuntimeManager", () => {
const declarations = await listProjectDeclarations(manager);
expect(declarations.resources.map((resource) => `${resource.type}.${resource.id}`)).toEqual(["agent.assistant"]);
});
test("uses Build-inferred Environment and Vault bindings in the Workbench runtime", async () => {
const directory = await initializedProject("automatic-bindings");
for (const [resourcePath, declaration] of [
["environments/dev/environment.json", { id: "dev", config: { type: "cloud" } }],
["vaults/secrets/vault.json", { id: "secrets", display_name: "Secrets", credentials: [] }],
] as const) {
const path = join(directory, "agents/assistant", resourcePath);
await mkdir(join(path, ".."), { recursive: true });
await writeFile(path, JSON.stringify(declaration));
}
const manager = trackManager(new ProjectRuntimeManager(directory));
await manager.ensureStarted();
const snapshot = manager.getSnapshot();
expect(snapshot.status).toBe("valid");
expect(snapshot.config?.agents.assistant).toMatchObject({ environment: "dev", vault: "secrets" });
const agentPath = join(directory, "agents/assistant/agent.json");
expect(JSON.parse(await readFile(agentPath, "utf8"))).not.toHaveProperty("environment");
await commitProjectBuild({ projectRoot: directory, baseRevision: snapshot.revision! });
await manager.refreshAfterSourceMutation();
expect(JSON.parse(await readFile(agentPath, "utf8"))).toMatchObject({ environment: "dev", vault: "secrets" });
expect(manager.getSnapshot().config?.agents.assistant).toMatchObject({ environment: "dev", vault: "secrets" });
});

test("reloads generated Vault references using the project-local .env after Build", async () => {
const directory = await initializedProject("vault-build");
const vaultPath = join(directory, "agents/assistant/vaults/secrets/vault.json");
Expand Down
2 changes: 1 addition & 1 deletion apps/webui/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const LANGUAGE_STORAGE_KEY = "openagentpack.workbench.language";

function detectedLanguage(): SupportedLanguage {
if (typeof window === "undefined") return "en-US";
return normalizeLanguage(window.localStorage.getItem(LANGUAGE_STORAGE_KEY) ?? window.navigator.language);
return normalizeLanguage(window.localStorage.getItem(LANGUAGE_STORAGE_KEY) ?? "en-US");
}

void i18n.use(initReactI18next).init({
Expand Down
63 changes: 63 additions & 0 deletions apps/webui/tests/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,70 @@ function leafKeys(value: object, prefix = ""): string[] {
});
}

function loadWorkbenchLanguage(storedLanguage: string | null, nextLanguage?: string) {
const entryUrl = new URL("../src/i18n/index.ts", import.meta.url).href;
// Isolate browser globals and the i18next singleton from other tests.
const result = Bun.spawnSync({
cmd: [
process.execPath,
"--eval",
`
const preferences = new Map([["openagentpack.workbench.language", ${JSON.stringify(storedLanguage)}]]);
globalThis.window = {
navigator: { language: "zh-CN" },
localStorage: {
getItem: (key) => preferences.get(key) ?? null,
setItem: (key, value) => preferences.set(key, value),
},
};
globalThis.document = { documentElement: { lang: "" } };
const { i18n, LANGUAGE_STORAGE_KEY } = await import(${JSON.stringify(entryUrl)});
const initialLanguage = i18n.resolvedLanguage;
const nextLanguage = ${JSON.stringify(nextLanguage) ?? "undefined"};
if (nextLanguage) await i18n.changeLanguage(nextLanguage);
console.log(JSON.stringify({
initialLanguage,
language: i18n.resolvedLanguage,
storedLanguage: preferences.get(LANGUAGE_STORAGE_KEY),
documentLanguage: document.documentElement.lang,
}));
`,
],
stdout: "pipe",
stderr: "pipe",
});
expect(result.exitCode).toBe(0);
return JSON.parse(result.stdout.toString().trim().split("\n").at(-1) ?? "");
}

describe("Workbench translations", () => {
test("defaults to English even when the browser language is Chinese", () => {
expect(loadWorkbenchLanguage(null)).toEqual({
initialLanguage: "en-US",
language: "en-US",
storedLanguage: "en-US",
documentLanguage: "en-US",
});
});

test("preserves an explicitly saved language preference", () => {
expect(loadWorkbenchLanguage("zh-CN").initialLanguage).toBe("zh-CN");
expect(loadWorkbenchLanguage("en-US").initialLanguage).toBe("en-US");
});

test("falls back to English for an unsupported saved language", () => {
expect(loadWorkbenchLanguage("fr-FR").initialLanguage).toBe("en-US");
});

test("still switches languages and persists the user's selection", () => {
expect(loadWorkbenchLanguage(null, "zh-CN")).toEqual({
initialLanguage: "en-US",
language: "zh-CN",
storedLanguage: "zh-CN",
documentLanguage: "zh-CN",
});
});

test("English and Chinese resources expose the same keys", () => {
expect(leafKeys(zhCN).sort()).toEqual(leafKeys(enUS).sort());
});
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

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

4 changes: 3 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ agents init

The init wizard asks two questions — which provider(s) to use and what to name your first agent — then writes a starter `agents.yaml`. This is the compact YAML workflow used by `validate → plan → apply` and `agents playground`.

For a locally managed multi-file project and Workbench, start with `agents project init` instead. It creates `project.json`, `agents/assistant/agent.json`, and `instructions.md`, plus a Git-independent full-tree baseline. It also adds Skill/File/Vault/Environment samples and bilingual README files under each resource directory's `_examples/`. These samples are not referenced by the Agent and are excluded from Build/remote Publish; copy one outside `_examples/` and configure its Agent reference to enable it. Directory projects always use Bailian, so `project.json` does not contain Provider configuration. Environment, Vault, Memory Store, File, and Skill declarations belong in the Agent directory (or root shared-resource directories), not in `project.json`. For Agent-local content, Build automatically generates Skill metadata and its Agent reference from a directory containing `SKILL.md`. A File can be copied directly into the Agent's `files/` directory or placed first in a resource-ID directory containing one content file; Build generates its metadata and `/mnt/<name>` Agent mount in either case. Explicit JSON always wins. Use `agents project validate`, `project build`, `project publish`, `project workbench`, and `project version ...`. The two workflows are intentionally separate: YAML Apply does not create directory versions, while project Publish consumes only `.openagentpack/build/agents.yaml` and never builds implicitly.
For a locally managed multi-file project and Workbench, start with `agents project init` instead. It creates `project.json`, `agents/assistant/agent.json`, and `instructions.md`, plus a Git-independent full-tree baseline. It also adds Skill/File/Vault/Environment samples and bilingual README files under each resource directory's `_examples/`. These samples are not referenced by the Agent and are excluded from Build/remote Publish; copy one outside `_examples/` into the owning Agent's resource directory, then Build adds its reference. Directory projects always use Bailian, so `project.json` does not contain Provider configuration. Environment, Vault, Memory Store, File, and Skill declarations belong in the Agent directory (or root shared-resource directories), not in `project.json`. Build links all active Agent-local resources, including those with existing metadata JSON. List references are appended without duplicates; Environment and Vault select the sole local candidate only when no explicit binding exists. Multiple candidates require an explicit selection. Existing references, Skill versions, and File mount paths are preserved; shared root resources still require explicit references. Provider capability validation remains enforced, so Bailian Memory Stores are still rejected. Build can also generate missing Skill metadata from `SKILL.md`, or File metadata from a file copied directly into `files/` or a resource-ID directory containing one content file. New File mounts default to `/mnt/<source basename>`. Use `agents project validate`, `project build`, `project publish`, `project workbench`, and `project version ...`. The two workflows are intentionally separate: YAML Apply does not create directory versions, while project Publish consumes only `.openagentpack/build/agents.yaml` and never builds implicitly.

Directory Init defaults to a new `managed-agent/` subdirectory in the current working directory. Run `cd managed-agent` before subsequent project commands, or pass `--project ./managed-agent`. To initialize in place or convert the current `agents.yaml`, explicitly use `agents project init --project .`. Build writes local files without confirmation; use `--dry-run` for a read-only preview. Publish still requires confirmation before remote changes.

The generated file for the `bailian` provider and an agent named `assistant` looks like this:

Expand Down
4 changes: 3 additions & 1 deletion docs/getting-started.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ agents init

init 向导问两个问题 —— 选哪个/哪些 Provider、给第一个 agent 起什么名 —— 然后生成 `agents.yaml`。这是供 `validate → plan → apply` 与 `agents playground` 使用的紧凑 YAML 流程。

如需本地多文件项目和 Workbench,请改用 `agents project init`。它会创建 `project.json`、`agents/assistant/agent.json`、`instructions.md`,并建立不依赖 Git 的全目录基线版本。四类资源目录的 `_examples/` 下会生成完整配置示例和中英文 README;示例不写入 Agent 引用、不进入 Build 或远端 Publish,需要使用时复制到 `_examples/` 外并配置 Agent 引用。目录项目固定使用百炼,因此 `project.json` 不再包含 Provider 配置。Environment、Vault、Memory Store、File 和 Skill 声明放在 Agent 目录(或根共享资源目录),不再写入 `project.json`。对于 Agent 本地内容,Build 会为包含 `SKILL.md` 的目录自动生成 Skill 元数据并写入 Agent 引用;复制到 Agent `files/` 目录的文件可以直接放置,也可以先放入以资源 ID 命名且只含一个内容文件的子目录,Build 都会自动生成 File 元数据和 `/mnt/<文件名>` 挂载。显式 JSON 始终优先。后续使用 `agents project validate`、`project build`、`project publish`、`project workbench` 与 `project version ...`。两套流程明确隔离:传统 YAML Apply 不产生目录版本;project Publish 只使用 `.openagentpack/build/agents.yaml`,且不会隐式执行 Build。
如需本地多文件项目和 Workbench,请改用 `agents project init`。它会创建 `project.json`、`agents/assistant/agent.json`、`instructions.md`,并建立不依赖 Git 的全目录基线版本。四类资源目录的 `_examples/` 下会生成完整配置示例和中英文 README;示例不写入 Agent 引用、不进入 Build 或远端 Publish,需要使用时复制到所属 Agent 资源目录的 `_examples/` 外,再由 Build 自动关联。目录项目固定使用百炼,因此 `project.json` 不再包含 Provider 配置。Environment、Vault、Memory Store、File 和 Skill 声明放在 Agent 目录(或根共享资源目录),不再写入 `project.json`。Build 会关联所有已启用的 Agent 本地资源,包括已有元数据 JSON 的资源:列表引用只追加缺失项;未指定 Environment、Vault 时自动关联唯一候选,多个候选则要求显式选择。已有引用、Skill 版本和 File 挂载路径均保留;根目录共享资源仍需显式引用。Provider 能力校验不变,百炼目前仍不支持 Memory Store。Build 也会为只有 `SKILL.md` 的目录生成 Skill 元数据,为直接放入 `files/` 或资源 ID 子目录中唯一的内容文件生成 File 元数据,新增挂载默认使用 `/mnt/<源文件名>`。后续使用 `agents project validate`、`project build`、`project publish`、`project workbench` 与 `project version ...`。两套流程明确隔离:传统 YAML Apply 不产生目录版本;project Publish 只使用 `.openagentpack/build/agents.yaml`,且不会隐式执行 Build。

目录 Init 默认在当前工作目录下创建 `managed-agent/` 子目录。后续项目操作请先执行 `cd managed-agent`,或传入 `--project ./managed-agent`。如需在当前目录初始化或转换已有的 `agents.yaml`,请显式执行 `agents project init --project .`。Build 无需确认即可写入本地文件;使用 `--dry-run` 可只读预览。Publish 变更远端资源前仍需确认。

为 `bailian` provider、agent 名为 `assistant` 生成的文件如下:

Expand Down
Loading