Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b6a83ed
Show the Feishu process as one timeline under a colored header
SaladDay Oct 25, 2026
75b8a81
Fold Feishu tool calls into one row and add /clear and /stop
SaladDay Oct 25, 2026
08ba416
Move the Feishu running status into the header and tint folded history
SaladDay Oct 25, 2026
5020762
Keep Feishu results in their own card and Markdown images out of cards
SaladDay Oct 25, 2026
837d71a
Show test and trial results on their buttons
SaladDay Oct 26, 2026
7b11f73
Use Core's saved Agents and Environment Templates
SaladDay Oct 26, 2026
aaaf6dc
Unify the capability library and give the bound bot's identity to eve…
SaladDay Oct 26, 2026
998ce5b
Count a message sent during a running Turn as part of that Turn
SaladDay Oct 26, 2026
caa3d77
Request im:message.group_msg when a bot is created by scanning
SaladDay Oct 26, 2026
13202a0
Generate Parsar's Harness options from the Harness declarations
SaladDay Oct 26, 2026
4b55a5d
Merge main: confirm uncertain sends and recover from creation-time co…
SaladDay Oct 26, 2026
a7da3fb
Request common Feishu scopes, name each message's source and link mis…
SaladDay Oct 26, 2026
3032d22
Keep Feishu chats tidy and drop the usual-time hint
SaladDay Oct 26, 2026
dd1d450
Keep one Feishu private-chat thread per sender
SaladDay Oct 26, 2026
712f049
Keep every step in Feishu cards and never send people to the Web
SaladDay Oct 26, 2026
d8fabe6
Delete a Feishu bot at once
SaladDay Oct 26, 2026
af41625
Request only Feishu scopes the organization grants without review
SaladDay Oct 26, 2026
2169648
Name a Feishu bot after its app as soon as it is added
SaladDay Oct 26, 2026
c8c640b
Keep steers recorded and ask only for scopes the Agent names
SaladDay Oct 26, 2026
29aaf0c
Hand files to Agents through Core's Files API, up to 50 MiB
SaladDay Oct 26, 2026
1445664
Merge main: carry environment network access into saved Templates
SaladDay Oct 29, 2026
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
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ The runnable test-only example [`testdata/onboarding/main.go`](https://github.co

## Declare the Harness

Each Harness is one `var Declaration = harnessconfig.Declaration{...}` in `internal/harnessconfig/<package>/declaration.go`. The package name is the kind without underscores. Creating the file registers the Harness: `make generate-harness-catalog` generates the Core registry, the Runtime's `harnessDeclarations`, the client identifiers and display names and the [registration reference](./harness-catalog.md), and `make check-harness-catalog` rejects stale projections. `make openapi` derives the Harness enums from the same declarations, so do not add handwritten enums to DTO tags or route annotations. There is no dynamic plugin loader, and a declaration cannot state a machine's availability.
Each Harness is one `var Declaration = harnessconfig.Declaration{...}` in `internal/harnessconfig/<package>/declaration.go`. The package name is the kind without underscores. Creating the file registers the Harness: `make generate-harness-catalog` generates the Core registry, the Runtime's `harnessDeclarations`, the client identifiers and display names, the Parsar example's protocols and execution options and the [registration reference](./harness-catalog.md), and `make check-harness-catalog` rejects stale projections. `make openapi` derives the Harness enums from the same declarations, so do not add handwritten enums to DTO tags or route annotations. There is no dynamic plugin loader, and a declaration cannot state a machine's availability.

The declaration holds:

Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/harness-onboarding.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Harness 协议"
source: contracts/agents-api/harness-onboarding.md
source_hash: 258dc7cba6a5b2959d9b7b525ab5a6a9ef4ed53939e44276773b71095467946e
source_hash: 4ab0e6623919424df5a56d302e3070bd2b59ad43416bce08547c4b136a33e4a4
---

**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code);**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。Harness 协议是 Runtime 与适配器之间的契约:适配器接口及其生命周期义务、声明、注册和验收。本文面向添加或维护 Harness 的开发者。
Expand Down Expand Up @@ -178,7 +178,7 @@ Runtime 统一解析[状态目录](../../../docs/zh/configuration.md#runtime-res

## 声明 Harness {#declare-the-harness}

每个 Harness 是 `internal/harnessconfig/<package>/declaration.go` 中的一个 `var Declaration = harnessconfig.Declaration{...}`。包名是去掉下划线的 kind。创建该文件即注册 Harness:`make generate-harness-catalog` 生成 Core 注册表、Runtime 的 `harnessDeclarations`、客户端标识符和显示名称以及[注册参考](harness-catalog.md),`make check-harness-catalog` 拒绝过期的投影。`make openapi` 从同一批声明派生 Harness 枚举,因此不要在 DTO 标签或路由注解中手写枚举。没有动态插件加载器,声明也不能表示机器的可用性。
每个 Harness 是 `internal/harnessconfig/<package>/declaration.go` 中的一个 `var Declaration = harnessconfig.Declaration{...}`。包名是去掉下划线的 kind。创建该文件即注册 Harness:`make generate-harness-catalog` 生成 Core 注册表、Runtime 的 `harnessDeclarations`、客户端标识符和显示名称、Parsar 示例使用的协议和执行选项,以及[注册参考](harness-catalog.md),`make check-harness-catalog` 拒绝过期的投影。`make openapi` 从同一批声明派生 Harness 枚举,因此不要在 DTO 标签或路由注解中手写枚举。没有动态插件加载器,声明也不能表示机器的可用性。

声明包含:

Expand Down
44 changes: 24 additions & 20 deletions example/parsar/README.md

Large diffs are not rendered by default.

44 changes: 24 additions & 20 deletions example/parsar/README.zh.md

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions example/parsar/examples/feishu/environment.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@
"LARKSUITE_CLI_CONFIG_DIR": "/workspace/.lark-cli",
"LARKSUITE_CLI_DATA_DIR": "/workspace/.lark-cli/data"
},
"bot_env": {
"FEISHU_APP_ID": "app_id",
"FEISHU_APP_SECRET": "app_secret",
"FEISHU_BRAND": "brand"
},
"credentials": ["FEISHU_APP_ID", "FEISHU_APP_SECRET"],
"files": [
{ "path": "/workspace/WORKSPACE.md", "content": "# 飞书工作环境\n\n官方 lark-cli 已在 Agent 启动前安装,以 Agent 绑定的飞书机器人身份运行。禁止输出 App Secret、令牌或环境变量。读取 Skill 后使用具体业务命令。网络错误与权限不足分别报告。产物写到 ./outputs。\n" }
]
Expand Down
6 changes: 3 additions & 3 deletions example/parsar/examples/manager/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ Write a JSON file, then call `PUT /app/runtimes/<that UUID> --file environment.j
"packages": { "npm": [], "python": ["openpyxl"] },
"setup_script": "mkdir -p /workspace/outputs",
"env": {},
"bot_env": {},
"credentials": [],
"files": [],
"prepare": false
}
```

Use only needed packages and files. Initial files are `{ "path": "/workspace/name", "content": "text" }`. Setup runs in the hosted workspace before the Agent starts; it is executable code, so review it as part of the requested configuration. Keep persistent files under `/workspace`; HOME is not persistent. Environment preparation may be reused. Never put the management CLI or its address in ordinary business templates unless the user explicitly asks for management access.

Environment variable values are write-only: GET returns `null`, and PUT with `null` keeps the saved value. Retain untouched names. Removing a name removes its value. Do not ask users to paste secrets into chat; direct them to the existing configuration UI. A template with `bot_env` needs an Agent bound to a Feishu bot before it can run.
Environment variable values are write-only: GET returns `null`, and PUT with `null` keeps the saved value. Retain untouched names. Removing a name removes its value. Do not ask users to paste secrets into chat; direct them to the existing configuration UI. `credentials` lists variables the Agent must supply. `FEISHU_APP_ID`, `FEISHU_APP_SECRET` and `FEISHU_BRAND` come from the Agent's bound Feishu bot, so list them when setup or a capability needs the bot's identity; an Agent without a bot cannot then be saved. Any other name is supplied from a secret the Agent chooses in its `credentials` map (name to `/app/secrets` ID). Skills (`PUT /app/skills/<skill_id>/credentials`) and Plugins declare the same list.

## Create an Agent

Expand Down Expand Up @@ -80,7 +80,7 @@ When a user asks to open or connect a Feishu bot, guide the whole setup rather t

Read `/app/agents`, `/app/runtimes` and `/app/feishu/bots`. Identify the intended business Agent from the user's goal; if it does not exist, create it using the workflow above. Do not bind the management Agent unless the user explicitly wants management access through that bot. Each bot binds one Agent and each Agent binds at most one bot. Reuse the intended Agent's existing bot; do not replace another binding, rotate credentials or create a duplicate to resolve a conflict. Ask only for unresolved choices such as which Agent or whether to use an existing Feishu application.

For a new setup, default to private chat only and tell the user. Enable group chat only when requested: it permits replies to mentions in any group the bot joins, not a list of selected group IDs. Preserve the chat choices of an existing bot unless the user asks to change them. Binding a bot supplies that bot's identity to new Agent conversations, including Web conversations when the environment maps `bot_env`; do not copy credentials into files or ordinary Agent instructions.
For a new setup, default to private chat only and tell the user. Enable group chat only when requested: it permits replies to mentions in any group the bot joins, not a list of selected group IDs. Preserve the chat choices of an existing bot unless the user asks to change them. Binding a bot supplies that bot's identity as `FEISHU_APP_ID`, `FEISHU_APP_SECRET` and `FEISHU_BRAND` to every new conversation of its Agent, Web ones included; do not copy credentials into files or ordinary Agent instructions.

### Register a new application

Expand Down
35 changes: 24 additions & 11 deletions example/parsar/server.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readMediaBytes } from "./shared/media.mjs";
import { maxUploadBytes, readMediaBytes } from "./shared/media.mjs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createServer } from "node:http";
Expand All @@ -9,6 +9,7 @@ import { buildDirectory } from "./paths.mjs";
import { isIPv4 } from "node:net";
import { openStore, dataPath, AppError } from "./server/store.mjs";
import { productAPI } from "./server/product.mjs";
import { boundAgents } from "./server/capability-kinds.mjs";
import { createFeishuTransport, feishuWorkers } from "./server/feishu.mjs";

const root = fileURLToPath(new URL(".", import.meta.url));
Expand All @@ -23,6 +24,7 @@ const routes = [
],
[/^\/v1\/agents\/environments\/[a-f0-9-]{36}$/, ["GET"]],
[/^\/v1\/agents\/environments\/[a-f0-9-]{36}\/files$/, ["GET", "POST"]],
[/^\/v1\/files$/, ["POST"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/artifacts$/, ["GET"]],
[
/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/artifacts\/[a-f0-9-]{36}\/content$/,
Expand All @@ -31,8 +33,6 @@ const routes = [
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}$/, ["GET"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/(items|turns)$/, ["GET"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/events$/, ["GET", "POST"]],
[/^\/v1\/agents$/, ["GET", "POST"]],
[/^\/v1\/agents\/[a-f0-9-]{36}$/, ["GET", "POST"]],
];
routes.push(
[
Expand Down Expand Up @@ -98,16 +98,18 @@ function fail(res, status, message) {

export function coreClient(config, fetchImpl = fetch) {
return async (path, method, body, key) => {
// A FormData body is a multipart upload, which sets its own Content-Type.
const form = body instanceof FormData;
const response = await fetchImpl(`${config.target}${path}`, {
method,
...(body === undefined ? {} : { body: JSON.stringify(body) }),
...(body === undefined ? {} : { body: form ? body : JSON.stringify(body) }),
redirect: "manual",
signal: AbortSignal.timeout(30_000),
signal: AbortSignal.timeout(form ? 300_000 : 30_000),
headers: {
...(key ? { "Idempotency-Key": key } : {}),
Authorization: `Bearer ${config.key}`,
"OpenAI-Beta": "agents=v1",
"Content-Type": "application/json",
...(form ? {} : { "Content-Type": "application/json" }),
},
});
if (response.status >= 300 && response.status < 400)
Expand Down Expand Up @@ -250,6 +252,10 @@ export function createHandler(
"A same-origin Origin header is required for writes.",
);
}
// Skills live in Core; one an Agent still binds is not deleted.
const skill = req.method === "DELETE" && url.pathname.match(/^\/v1\/skills\/(skill_[A-Za-z0-9_-]+)$/)?.[1];
if (skill && store && boundAgents(store, "skills", skill).length)
return fail(res, 409, "Skill 仍被 Agent 使用,请先在 Agent 中解除绑定。");
const streaming =
req.method === "GET" && url.pathname.endsWith("/events");
const controller = new AbortController();
Expand All @@ -267,10 +273,12 @@ export function createHandler(
size += chunk.length;
if (
size >
(url.pathname.startsWith("/v1/skills") ||
url.pathname.endsWith("/files")
? 8 * 1024 * 1024
: 1024 * 1024)
(url.pathname === "/v1/files"
? maxUploadBytes + 64 * 1024
: url.pathname.startsWith("/v1/skills") ||
url.pathname.endsWith("/files")
? 8 * 1024 * 1024
: 1024 * 1024)
)
return fail(res, 413, "Request too large.");
chunks.push(chunk);
Expand All @@ -281,7 +289,7 @@ export function createHandler(
"Content-Type": req.headers["content-type"] || "application/json",
...(streaming ? { Accept: "text/event-stream" } : {}),
};
if (url.pathname.startsWith("/v1/skills"))
if (url.pathname.startsWith("/v1/skills") || url.pathname === "/v1/files")
delete headers["OpenAI-Beta"];
if (req.headers["idempotency-key"])
headers["Idempotency-Key"] = req.headers["idempotency-key"];
Expand Down Expand Up @@ -420,6 +428,11 @@ if (
void productHandler.sessions
.refreshWarm()
.catch((error) => console.error("Prepared sessions:", error.message));
// Saved Core Agents and Templates that no longer resolve, as after moving to
// a new Project, are created again on next use.
await productHandler.resources
.verify()
.catch((error) => console.error("Core resources:", error.message));
refreshPrepared();
setInterval(refreshPrepared, 15000).unref();
// Running Turns are watched for stalls and failed Environments.
Expand Down
9 changes: 5 additions & 4 deletions example/parsar/server/capabilities.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AppError, text, uuid } from "./store.mjs";
import { boundAgents, credentialNames } from "./capability-kinds.mjs";
import { unzipSync } from "fflate";
const publicVersion = ({ archive, ...value }) => ({
...value,
has_archive: !!archive,
});
export const publicCapability = (row) => ({
const publicCapability = (row) => ({
...row,
versions: row.versions.map(publicVersion),
});
Expand All @@ -25,9 +26,7 @@ export function capabilityAPI(store) {
if (old && old.revision !== body.revision)
throw new AppError(409, "能力已更新,请重新加载。");
if (method === "DELETE") {
if (
store.list("agents").some((agent) => agent.capability_ids?.includes(id))
)
if (boundAgents(store, "capabilities", id).length)
throw new AppError(409, "能力仍被 Agent 引用。");
store.remove("capabilities", id);
return {};
Expand Down Expand Up @@ -107,6 +106,8 @@ export function capabilityAPI(store) {
kind,
versions,
default_version,
// A Plugin's programs may read variables the Agent supplies from its secrets.
credentials: kind === "plugin" ? credentialNames(body.credentials ?? old?.credentials) : [],
deprecated: body.deprecated === true,
revision: (old?.revision || 0) + 1,
};
Expand Down
Loading
Loading