Skip to content

Commit 8196ac0

Browse files
committed
feat(project): auto-associate agent resources during build
Default project init to a managed-agent subdirectory and report actionable project-root errors. Remove local Build confirmation while retaining Publish confirmation, automatically bind active Agent-local resources, and default Workbench to English while preserving saved language preferences. Update documentation and regression coverage for project initialization, resource bindings, version previews, and Workbench behavior.
1 parent 0084ddc commit 8196ac0

18 files changed

Lines changed: 545 additions & 56 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ The mechanics are a single `agents.yaml`, a `validate → plan → apply` workfl
7777
## Quick start
7878

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

9091
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.
9192

93+
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.
94+
9295
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.
9396

9497
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.

apps/server/tests/project-declarations.test.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, describe, expect, test } from "bun:test";
2-
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2+
import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import {
@@ -217,18 +217,36 @@ describe("directory project declaration editing", () => {
217217
expect(await readFile(statePath, "utf8")).toBe('{"remote":"latest"}\n');
218218
});
219219

220-
test("removes a File declaration without deleting its local source", async () => {
220+
test("protects an automatically associated Agent-local File even without an authored mount", async () => {
221221
const { directory, manager } = await projectFixture({ fileMount: false });
222222
const listed = await listProjectDeclarations(manager);
223+
expect(resource(listed.resources, "file", "input").references.map((reference) => reference.path)).toEqual([
224+
"agents.assistant.files",
225+
]);
226+
const preview = await previewDeclarationChange(
227+
{ type: "file", id: "input", baseRevision: listed.revision, action: "delete" },
228+
manager,
229+
);
230+
expect(preview.can_commit).toBe(false);
231+
expect((await stat(join(directory, "agents/assistant/files/input/file.json"))).isFile()).toBe(true);
232+
});
233+
234+
test("removes an unreferenced shared File declaration without deleting its local source", async () => {
235+
const { directory, manager } = await projectFixture({ fileMount: false });
236+
await mkdir(join(directory, "resources/files"), { recursive: true });
237+
const fileDirectory = join(directory, "resources/files/input");
238+
await rename(join(directory, "agents/assistant/files/input"), fileDirectory);
239+
await manager.refreshAfterSourceMutation();
240+
const listed = await listProjectDeclarations(manager);
223241
await commitDeclarationChange(
224242
{ type: "file", id: "input", baseRevision: listed.revision, action: "delete" },
225243
manager,
226244
);
227245

228-
expect(await stat(join(directory, "agents/assistant/files/input/file.json")).catch(() => null)).toBeNull();
229-
expect(await readFile(join(directory, "agents/assistant/files/input/input.txt"), "utf8")).toBe("Keep local file\n");
246+
expect(await stat(join(fileDirectory, "file.json")).catch(() => null)).toBeNull();
247+
expect(await readFile(join(fileDirectory, "input.txt"), "utf8")).toBe("Keep local file\n");
230248
expect(
231-
await stat(join(directory, "agents/assistant/files/input", FILE_AUTO_ASSOCIATION_IGNORE_FILE)).then(
249+
await stat(join(fileDirectory, FILE_AUTO_ASSOCIATION_IGNORE_FILE)).then(
232250
() => true,
233251
() => false,
234252
),

apps/server/tests/project-manager.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ describe("ProjectRuntimeManager", () => {
2929
const declarations = await listProjectDeclarations(manager);
3030
expect(declarations.resources.map((resource) => `${resource.type}.${resource.id}`)).toEqual(["agent.assistant"]);
3131
});
32+
test("uses Build-inferred Environment and Vault bindings in the Workbench runtime", async () => {
33+
const directory = await initializedProject("automatic-bindings");
34+
for (const [resourcePath, declaration] of [
35+
["environments/dev/environment.json", { id: "dev", config: { type: "cloud" } }],
36+
["vaults/secrets/vault.json", { id: "secrets", display_name: "Secrets", credentials: [] }],
37+
] as const) {
38+
const path = join(directory, "agents/assistant", resourcePath);
39+
await mkdir(join(path, ".."), { recursive: true });
40+
await writeFile(path, JSON.stringify(declaration));
41+
}
42+
const manager = trackManager(new ProjectRuntimeManager(directory));
43+
await manager.ensureStarted();
44+
const snapshot = manager.getSnapshot();
45+
expect(snapshot.status).toBe("valid");
46+
expect(snapshot.config?.agents.assistant).toMatchObject({ environment: "dev", vault: "secrets" });
47+
const agentPath = join(directory, "agents/assistant/agent.json");
48+
expect(JSON.parse(await readFile(agentPath, "utf8"))).not.toHaveProperty("environment");
49+
await commitProjectBuild({ projectRoot: directory, baseRevision: snapshot.revision! });
50+
await manager.refreshAfterSourceMutation();
51+
expect(JSON.parse(await readFile(agentPath, "utf8"))).toMatchObject({ environment: "dev", vault: "secrets" });
52+
expect(manager.getSnapshot().config?.agents.assistant).toMatchObject({ environment: "dev", vault: "secrets" });
53+
});
54+
3255
test("reloads generated Vault references using the project-local .env after Build", async () => {
3356
const directory = await initializedProject("vault-build");
3457
const vaultPath = join(directory, "agents/assistant/vaults/secrets/vault.json");

apps/webui/src/i18n/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const LANGUAGE_STORAGE_KEY = "openagentpack.workbench.language";
66

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

1212
void i18n.use(initReactI18next).init({

apps/webui/tests/i18n.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,70 @@ function leafKeys(value: object, prefix = ""): string[] {
88
});
99
}
1010

11+
function loadWorkbenchLanguage(storedLanguage: string | null, nextLanguage?: string) {
12+
const entryUrl = new URL("../src/i18n/index.ts", import.meta.url).href;
13+
// Isolate browser globals and the i18next singleton from other tests.
14+
const result = Bun.spawnSync({
15+
cmd: [
16+
process.execPath,
17+
"--eval",
18+
`
19+
const preferences = new Map([["openagentpack.workbench.language", ${JSON.stringify(storedLanguage)}]]);
20+
globalThis.window = {
21+
navigator: { language: "zh-CN" },
22+
localStorage: {
23+
getItem: (key) => preferences.get(key) ?? null,
24+
setItem: (key, value) => preferences.set(key, value),
25+
},
26+
};
27+
globalThis.document = { documentElement: { lang: "" } };
28+
const { i18n, LANGUAGE_STORAGE_KEY } = await import(${JSON.stringify(entryUrl)});
29+
const initialLanguage = i18n.resolvedLanguage;
30+
const nextLanguage = ${JSON.stringify(nextLanguage) ?? "undefined"};
31+
if (nextLanguage) await i18n.changeLanguage(nextLanguage);
32+
console.log(JSON.stringify({
33+
initialLanguage,
34+
language: i18n.resolvedLanguage,
35+
storedLanguage: preferences.get(LANGUAGE_STORAGE_KEY),
36+
documentLanguage: document.documentElement.lang,
37+
}));
38+
`,
39+
],
40+
stdout: "pipe",
41+
stderr: "pipe",
42+
});
43+
expect(result.exitCode).toBe(0);
44+
return JSON.parse(result.stdout.toString().trim().split("\n").at(-1) ?? "");
45+
}
46+
1147
describe("Workbench translations", () => {
48+
test("defaults to English even when the browser language is Chinese", () => {
49+
expect(loadWorkbenchLanguage(null)).toEqual({
50+
initialLanguage: "en-US",
51+
language: "en-US",
52+
storedLanguage: "en-US",
53+
documentLanguage: "en-US",
54+
});
55+
});
56+
57+
test("preserves an explicitly saved language preference", () => {
58+
expect(loadWorkbenchLanguage("zh-CN").initialLanguage).toBe("zh-CN");
59+
expect(loadWorkbenchLanguage("en-US").initialLanguage).toBe("en-US");
60+
});
61+
62+
test("falls back to English for an unsupported saved language", () => {
63+
expect(loadWorkbenchLanguage("fr-FR").initialLanguage).toBe("en-US");
64+
});
65+
66+
test("still switches languages and persists the user's selection", () => {
67+
expect(loadWorkbenchLanguage(null, "zh-CN")).toEqual({
68+
initialLanguage: "en-US",
69+
language: "zh-CN",
70+
storedLanguage: "zh-CN",
71+
documentLanguage: "zh-CN",
72+
});
73+
});
74+
1275
test("English and Chinese resources expose the same keys", () => {
1376
expect(leafKeys(zhCN).sort()).toEqual(leafKeys(enUS).sort());
1477
});

docs/getting-started.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ agents init
3838

3939
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`.
4040

41-
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.
41+
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.
42+
43+
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.
4244

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

docs/getting-started.zh-CN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ agents init
3838

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

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

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

0 commit comments

Comments
 (0)