From 48736da4dc2ed2c110f11a67058ebac05398f97a Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 11:26:10 +0800 Subject: [PATCH 01/15] feat(harness): accept a runtime parameter (adk default, codex) The Harness model now carries a `runtime` field (Literal["adk","codex"], default "adk") passed through to the Agent, so a deployed Harness server can run an agent on the codex runtime. Exposed on the CLI via `--runtime` on both `harness add` and `harness invoke` (one-time override). Docs + example updated. --- docs/content/docs/cli/harness-cli.en.mdx | 10 ++++--- docs/content/docs/cli/harness-cli.mdx | 4 ++- .../14_harness_server_on_agentkit/README.md | 5 ++-- .../README.zh.md | 3 ++- .../cli/test_cli_agentkit_harness_contract.py | 15 ++++++++++- tests/cloud/test_harness_app_contract.py | 17 ++++++++++++ veadk/cli/cli_agentkit.py | 27 ++++++++++++++++--- veadk/cloud/harness_app.py | 5 ++++ 8 files changed, 73 insertions(+), 13 deletions(-) diff --git a/docs/content/docs/cli/harness-cli.en.mdx b/docs/content/docs/cli/harness-cli.en.mdx index 37c9f171..a0092ba4 100644 --- a/docs/content/docs/cli/harness-cli.en.mdx +++ b/docs/content/docs/cli/harness-cli.en.mdx @@ -54,6 +54,7 @@ veadk agentkit harness add \ | `--system-prompt` | No | System prompt; defaults to `You are a helpful assistant.`. | | `--tools` | No | Comma-separated built-in tool names, e.g. `web_search,web_fetch`. | | `--skills` | No | Comma-separated skill hub names, e.g. `clawhub/lgwventrue/system-file-handler`. | +| `--runtime` | No | Agent runtime backend, `adk` (default) or `codex`. `codex` requires the optional codex extra on the server. | If a harness with the same name already exists, the server returns `code: 400` @@ -82,13 +83,14 @@ veadk agentkit harness invoke \ | `--system-prompt` | No | Override the system prompt for this call only (creates a one-time harness). | | `--tools` | No | Override tools for this call only, comma-separated (creates a one-time harness). | | `--skills` | No | Override skills for this call only, comma-separated (creates a one-time harness). | +| `--runtime` | No | Override the runtime for this call only, `adk` or `codex` (creates a one-time harness). | ### One-time harness override -If any of `--model-name` / `--system-prompt` / `--tools` / `--skills` is passed -to `invoke`, the server builds a **one-time harness** from those fields that -applies to this single call only — the stored harness of the same name is left -untouched: +If any of `--model-name` / `--system-prompt` / `--tools` / `--skills` / `--runtime` +is passed to `invoke`, the server builds a **one-time harness** from those fields +that applies to this single call only — the stored harness of the same name is +left untouched: ```bash veadk agentkit harness invoke \ diff --git a/docs/content/docs/cli/harness-cli.mdx b/docs/content/docs/cli/harness-cli.mdx index 2dea17d1..014549ad 100644 --- a/docs/content/docs/cli/harness-cli.mdx +++ b/docs/content/docs/cli/harness-cli.mdx @@ -48,6 +48,7 @@ veadk agentkit harness add \ | `--system-prompt` | 否 | 系统提示词,默认 `You are a helpful assistant.`。 | | `--tools` | 否 | 逗号分隔的内置工具名,如 `web_search,web_fetch`。 | | `--skills` | 否 | 逗号分隔的技能 hub 名,如 `clawhub/lgwventrue/system-file-handler`。 | +| `--runtime` | 否 | 智能体运行时,`adk`(默认)或 `codex`。`codex` 需服务端安装对应可选依赖。 | 若同名 harness 已存在,服务端返回 `code: 400`,不会覆盖。 @@ -74,10 +75,11 @@ veadk agentkit harness invoke \ | `--system-prompt` | 否 | 仅本次调用覆盖系统提示词(生成一次性 harness)。 | | `--tools` | 否 | 仅本次调用覆盖工具,逗号分隔(生成一次性 harness)。 | | `--skills` | 否 | 仅本次调用覆盖技能,逗号分隔(生成一次性 harness)。 | +| `--runtime` | 否 | 仅本次调用覆盖运行时,`adk` 或 `codex`(生成一次性 harness)。 | ### 一次性 harness 覆盖 -`invoke` 时若提供了 `--model-name` / `--system-prompt` / `--tools` / `--skills` 中的任意一个,服务端会用这些字段构造一个**一次性 harness**,仅对本次调用生效,不影响已注册的同名 harness: +`invoke` 时若提供了 `--model-name` / `--system-prompt` / `--tools` / `--skills` / `--runtime` 中的任意一个,服务端会用这些字段构造一个**一次性 harness**,仅对本次调用生效,不影响已注册的同名 harness: ```bash veadk agentkit harness invoke \ diff --git a/examples/14_harness_server_on_agentkit/README.md b/examples/14_harness_server_on_agentkit/README.md index 4f3fcd64..6953304e 100644 --- a/examples/14_harness_server_on_agentkit/README.md +++ b/examples/14_harness_server_on_agentkit/README.md @@ -39,8 +39,9 @@ python -m veadk.cloud.harness_app # serves the API on 0.0.0.0:8000 added** harness. A non-null `harness` overrides the stored one for this call (`overwrite: true`). -`harness` fields: `model_name`, `system_prompt`, and `tools`. `tools` accepts -either a list (`["web_search", "web_fetch"]`) or a comma-separated string +`harness` fields: `model_name`, `system_prompt`, `tools`, `skills`, and +`runtime` (`"adk"` default, or `"codex"`). `tools` accepts either a list +(`["web_search", "web_fetch"]`) or a comma-separated string (`"web_search,web_fetch"`). `run_agent_request` fields: `user_id`, `session_id`. Built-in tool names come from `veadk.tools.list_builtin_tools()` (e.g. diff --git a/examples/14_harness_server_on_agentkit/README.zh.md b/examples/14_harness_server_on_agentkit/README.zh.md index d18abfc5..3ac8b5e2 100644 --- a/examples/14_harness_server_on_agentkit/README.zh.md +++ b/examples/14_harness_server_on_agentkit/README.zh.md @@ -36,7 +36,8 @@ python -m veadk.cloud.harness_app # 在 0.0.0.0:8000 提供 API `{prompt, harness_name, harness?, run_agent_request}`,运行一个**已注册**的 harness。请求里带非空 `harness` 则对本次调用临时覆盖(`overwrite: true`)。 -`harness` 字段:`model_name`、`system_prompt`、`tools`。`tools` 既接受数组 +`harness` 字段:`model_name`、`system_prompt`、`tools`、`skills`、`runtime` +(运行时,默认 `"adk"`,可传 `"codex"`)。`tools` 既接受数组 (`["web_search", "web_fetch"]`)也接受逗号分隔字符串(`"web_search,web_fetch"`)。 `run_agent_request` 字段:`user_id`、`session_id`。 diff --git a/tests/cli/test_cli_agentkit_harness_contract.py b/tests/cli/test_cli_agentkit_harness_contract.py index bea5f1ea..c9fba73b 100644 --- a/tests/cli/test_cli_agentkit_harness_contract.py +++ b/tests/cli/test_cli_agentkit_harness_contract.py @@ -46,6 +46,7 @@ def test_option_names(self): "system_prompt", "tools", "skills", + "runtime", "url", "key", } @@ -54,9 +55,20 @@ def test_required_flags(self): assert self.opts["name"].required is True assert self.opts["url"].required is True # Optional ones must stay optional. - for name in ("model_name", "system_prompt", "tools", "skills", "key"): + for name in ( + "model_name", + "system_prompt", + "tools", + "skills", + "runtime", + "key", + ): assert self.opts[name].required is False + def test_runtime_choices(self): + assert isinstance(self.opts["runtime"].type, click.Choice) + assert set(self.opts["runtime"].type.choices) == {"adk", "codex"} + def test_default_system_prompt(self): assert self.opts["system_prompt"].default == "You are a helpful assistant." @@ -79,6 +91,7 @@ def test_parameters(self): "system_prompt", "tools", "skills", + "runtime", "user_id", "session_id", "url", diff --git a/tests/cloud/test_harness_app_contract.py b/tests/cloud/test_harness_app_contract.py index ee8d0d1c..8fbcdd74 100644 --- a/tests/cloud/test_harness_app_contract.py +++ b/tests/cloud/test_harness_app_contract.py @@ -21,6 +21,7 @@ """ import inspect +from unittest import mock from veadk.cloud import harness_app from veadk.cloud.harness_app import ( @@ -47,6 +48,7 @@ def test_fields(self): "tools", "skills", "system_prompt", + "runtime", } def test_defaults(self): @@ -55,6 +57,7 @@ def test_defaults(self): assert fields["tools"].default == "" assert fields["skills"].default == "" assert fields["system_prompt"].default == "You are a helpful assistant." + assert fields["runtime"].default == "adk" def test_tools_and_skills_are_csv_strings(self): # The server splits these with _split_csv(); they must stay plain @@ -110,6 +113,20 @@ def test_routes_registered(self): paths = {getattr(route, "path", None) for route in app.app.routes} assert {"/harness/add", "/harness/invoke"} <= paths + def test_create_agent_passes_runtime(self): + # _create_agent must forward the harness runtime to the Agent. Mock Agent + # so no model client is built (keeps the test offline). + app = HarnessApp() + with mock.patch.object(harness_app, "Agent") as agent_cls: + app._create_agent(Harness(runtime="codex")) + assert agent_cls.call_args.kwargs["runtime"] == "codex" + + def test_create_agent_defaults_runtime_to_adk(self): + app = HarnessApp() + with mock.patch.object(harness_app, "Agent") as agent_cls: + app._create_agent(Harness()) + assert agent_cls.call_args.kwargs["runtime"] == "adk" + class TestSplitCsv: def test_splits_and_trims(self): diff --git a/veadk/cli/cli_agentkit.py b/veadk/cli/cli_agentkit.py index 483e8c7a..0b136503 100644 --- a/veadk/cli/cli_agentkit.py +++ b/veadk/cli/cli_agentkit.py @@ -91,6 +91,12 @@ def harness() -> None: default=None, help="Comma-separated skill hub names, e.g. clawhub/lgwventrue/system-file-handler.", ) +@click.option( + "--runtime", + default=None, + type=click.Choice(["adk", "codex"]), + help="Agent runtime backend (defaults to the server's 'adk').", +) @click.option( "--url", required=True, @@ -103,7 +109,9 @@ def harness() -> None: envvar="HARNESS_KEY", help="Gateway API key for Bearer auth (or set HARNESS_KEY).", ) -def harness_add(name, model_name, system_prompt, tools, skills, url, key) -> None: +def harness_add( + name, model_name, system_prompt, tools, skills, runtime, url, key +) -> None: """Register a new harness on the server.""" spec: dict = {"system_prompt": system_prompt} # Pass the comma-separated strings through; the server splits them. @@ -113,6 +121,8 @@ def harness_add(name, model_name, system_prompt, tools, skills, url, key) -> Non spec["skills"] = skills if model_name: spec["model_name"] = model_name + if runtime: + spec["runtime"] = runtime result = _harness_request( url, "/harness/add", key, {"harness_name": name, "harness": spec} ) @@ -146,6 +156,12 @@ def harness_add(name, model_name, system_prompt, tools, skills, url, key) -> Non default=None, help="Override skills for this call, comma-separated (creates a one-time harness).", ) +@click.option( + "--runtime", + default=None, + type=click.Choice(["adk", "codex"]), + help="Override the runtime for this call (creates a one-time harness).", +) @click.option( "--user-id", "user_id", default="cli-user", help="User id for the session." ) @@ -171,6 +187,7 @@ def harness_invoke( system_prompt, tools, skills, + runtime, user_id, session_id, url, @@ -182,9 +199,9 @@ def harness_invoke( "harness_name": harness_name, "run_agent_request": {"user_id": user_id, "session_id": session_id}, } - # Any of --model-name/--system-prompt/--tools/--skills builds a one-time - # harness that overrides the stored one for this single call (the server - # replaces the whole agent). tools/skills are passed through as + # Any of --model-name/--system-prompt/--tools/--skills/--runtime builds a + # one-time harness that overrides the stored one for this single call (the + # server replaces the whole agent). tools/skills are passed through as # comma-separated strings; the server splits them. once: dict = {} if model_name: @@ -195,6 +212,8 @@ def harness_invoke( once["tools"] = tools if skills: once["skills"] = skills + if runtime: + once["runtime"] = runtime if once: body["harness"] = once result = _harness_request(url, "/harness/invoke", key, body) diff --git a/veadk/cloud/harness_app.py b/veadk/cloud/harness_app.py index 90c67a03..58a97e6b 100644 --- a/veadk/cloud/harness_app.py +++ b/veadk/cloud/harness_app.py @@ -17,6 +17,7 @@ import shutil import zipfile from pathlib import Path +from typing import Literal import frontmatter import httpx @@ -114,6 +115,9 @@ class Harness(BaseModel): tools: str = Field(default="") skills: str = Field(default="") system_prompt: str = Field(default="You are a helpful assistant.") + # Agent runtime backend: "adk" (default) or "codex". Passed through to the + # Agent; "codex" requires the optional codex extra on the server. + runtime: Literal["adk", "codex"] = Field(default="adk") class AddHarnessRequest(BaseModel): @@ -247,6 +251,7 @@ def _create_agent(self, harness: Harness) -> Agent: model_name=harness.model_name, instruction=harness.system_prompt, tools=tools, + runtime=harness.runtime, ) return agent From e748faa64d10f37df46c2de216fc316b3906533e Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 15:08:00 +0800 Subject: [PATCH 02/15] feat(harness): add deployable harness_app package and `veadk harness` CLI - veadk/cloud/harness_app/: env-assembled agent + FastAPI harness server (/harness/invoke with clone-based once-time overrides; incremental, deduped tools/skills; per-invoke skill temp-dir cleanup) - veadk harness create/deploy/invoke; deploy builds the image via AgentKit and creates an AgentKit runtime (Name=HARNESS_NAME, tag "Harness") - invoke override flags are generated from HarnessOverrides - replaces the single-file veadk/cloud/harness_app.py --- docs/content/docs/cli/harness-cli.en.mdx | 72 +++- docs/content/docs/cli/harness-cli.mdx | 58 ++- veadk/cli/cli.py | 2 + veadk/cli/cli_harness.py | 487 +++++++++++++++++++++++ veadk/cloud/harness_app.py | 267 ------------- veadk/cloud/harness_app/Dockerfile | 34 ++ veadk/cloud/harness_app/__init__.py | 13 + veadk/cloud/harness_app/agent.py | 37 ++ veadk/cloud/harness_app/app.py | 120 ++++++ veadk/cloud/harness_app/types.py | 97 +++++ veadk/cloud/harness_app/utils.py | 341 ++++++++++++++++ 11 files changed, 1257 insertions(+), 271 deletions(-) create mode 100644 veadk/cli/cli_harness.py delete mode 100644 veadk/cloud/harness_app.py create mode 100644 veadk/cloud/harness_app/Dockerfile create mode 100644 veadk/cloud/harness_app/__init__.py create mode 100644 veadk/cloud/harness_app/agent.py create mode 100644 veadk/cloud/harness_app/app.py create mode 100644 veadk/cloud/harness_app/types.py create mode 100644 veadk/cloud/harness_app/utils.py diff --git a/docs/content/docs/cli/harness-cli.en.mdx b/docs/content/docs/cli/harness-cli.en.mdx index a0092ba4..e1c9f4ff 100644 --- a/docs/content/docs/cli/harness-cli.en.mdx +++ b/docs/content/docs/cli/harness-cli.en.mdx @@ -11,12 +11,80 @@ The Harness server exposes `/harness/add` and `/harness/invoke`, letting you register and invoke harnesses at runtime without redeploying. - Before you start, deploy the Harness server to Volcengine AgentKit (see the + Before you start, deploy the Harness server to Volcengine AgentKit (see + [Deploying the Harness server](#deploying-the-harness-server) below, or the [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit) example) and grab its **endpoint URL** and gateway **API key**. -## Command reference +## Deploying the Harness server + +The top-level `veadk harness` group scaffolds and deploys the Harness server +itself (this is separate from the `veadk agentkit harness` client below). + +| Command | Description | +| :-- | :-- | +| `veadk harness create ` | Scaffold a deployable harness directory. | +| `veadk harness deploy` | Build and push the harness image, then create an AgentKit runtime. | + +### `harness create` + +Create a deployment directory documenting every harness environment variable: + +```bash +veadk harness create my-harness +``` + +This writes: + +- `.env.example` — every harness env var (`MODEL_AGENT_NAME`, `MODEL_AGENT_API_KEY`, + `MODEL_AGENT_API_BASE`, `MODEL_AGENT_PROVIDER`, `SYSTEM_PROMPT`, `TOOLS`, `SKILLS`, + `RUNTIME`, `APP_NAME`, `KNOWLEDGEBASE_TYPE`, `LONGTERM_MEM_TYPE`, + `SHORTTERM_MEM_TYPE`, `HARNESS_NAME`, `SKILL_HUB_DOWNLOAD_URL`, `SERVER_HOST`, + `SERVER_PORT`) with comments and placeholders. +- `Dockerfile` — builds the image that serves the harness app. +- `agentkit.yaml` — AgentKit build config (hybrid: local build + push to + Container Registry); `ve_runtime_name` defaults to the harness name. +- `README.md` — quickstart. + +### `harness deploy` + +Copy `.env.example` to `.env`, fill it in (set `HARNESS_NAME`), set a real +`cr_instance_name` in `agentkit.yaml` (or run `agentkit config`), then from +inside the directory: + +```bash +cd my-harness +cp .env.example .env # then edit it (set HARNESS_NAME) +# edit agentkit.yaml: set cr_instance_name (or run `agentkit config`) +veadk harness deploy +``` + +| Option | Required | Description | +| :-- | :-- | :-- | +| `--volcengine-access-key` | No | Volcengine access key (defaults to `VOLCENGINE_ACCESS_KEY` / `VOLC_ACCESSKEY`). | +| `--volcengine-secret-key` | No | Volcengine secret key (defaults to `VOLCENGINE_SECRET_KEY` / `VOLC_SECRETKEY`). | +| `--region` | No | AgentKit region (default `cn-beijing` or `VOLCENGINE_REGION`). | +| `--role-name` | No | Runtime IAM role name (default: auto-generated or `HARNESS_RUNTIME_ROLE`). | +| `--path` | No | Harness directory, default `.`. | + +`deploy` builds and pushes the harness image via AgentKit's hybrid build (local +`docker build` + push to Container Registry), then calls AgentKit's +`CreateRuntime` to create a runtime whose: + +- **Name** is `HARNESS_NAME` from your `.env` (default `default` when unset), +- **Tags** carry a single tag whose key is the literal `Harness` (key only, no value), +- **Envs** are the directory's `.env`, +- **ArtifactType** is `image` and **ArtifactUrl** is the freshly pushed image. + +On success it prints the runtime's name and id; look up its endpoint in the +AgentKit console or via the AgentKit SDK (`GetRuntime`), then pass it to +`veadk agentkit harness invoke --url ...`. + +## Client command reference + +Once the server is deployed, `veadk agentkit harness` is the HTTP client used to +register and invoke harnesses against it at runtime. | Command | Description | | :-- | :-- | diff --git a/docs/content/docs/cli/harness-cli.mdx b/docs/content/docs/cli/harness-cli.mdx index 014549ad..17a8d801 100644 --- a/docs/content/docs/cli/harness-cli.mdx +++ b/docs/content/docs/cli/harness-cli.mdx @@ -8,10 +8,64 @@ description: "通过命令行注册并调用部署在 Harness server 上的 harn 一个 *harness* 就是一份具名的智能体规格 —— **模型 + 系统提示词 + 工具 + 技能**。Harness server 暴露 `/harness/add` 与 `/harness/invoke` 两个接口,让你在运行时动态注册并调用 harness,无需重新部署。 - 使用前需先把 Harness server 部署到火山引擎 AgentKit(参见示例 [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit)),并拿到服务的 **Endpoint URL** 与网关 **API Key**。 + 使用前需先把 Harness server 部署到火山引擎 AgentKit(参见下方 [部署 Harness server](#部署-harness-server),或示例 [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit)),并拿到服务的 **Endpoint URL** 与网关 **API Key**。 -## 命令一览 +## 部署 Harness server + +顶层命令组 `veadk harness` 用于脚手架生成并部署 Harness server 本身(与下文的 `veadk agentkit harness` 客户端命令相互独立)。 + +| 命令 | 描述 | +| :-- | :-- | +| `veadk harness create ` | 生成一个可部署的 harness 目录。 | +| `veadk harness deploy` | 构建并推送 harness 镜像,然后创建一个 AgentKit runtime。 | + +### `harness create` + +生成一个部署目录,并在其中以注释形式说明所有 harness 环境变量: + +```bash +veadk harness create my-harness +``` + +该命令会写入: + +- `.env.example` —— 列出全部 harness 环境变量(`MODEL_AGENT_NAME`、`MODEL_AGENT_API_KEY`、`MODEL_AGENT_API_BASE`、`MODEL_AGENT_PROVIDER`、`SYSTEM_PROMPT`、`TOOLS`、`SKILLS`、`RUNTIME`、`APP_NAME`、`KNOWLEDGEBASE_TYPE`、`LONGTERM_MEM_TYPE`、`SHORTTERM_MEM_TYPE`、`HARNESS_NAME`、`SKILL_HUB_DOWNLOAD_URL`、`SERVER_HOST`、`SERVER_PORT`),并附带注释与占位值。 +- `Dockerfile` —— 构建提供 harness 应用服务的镜像。 +- `agentkit.yaml` —— AgentKit 构建配置(hybrid:本地构建 + 推送到容器镜像仓库 CR),其中 `ve_runtime_name` 默认为 harness 名称。 +- `README.md` —— 快速开始说明。 + +### `harness deploy` + +将 `.env.example` 复制为 `.env` 并填好(务必设置 `HARNESS_NAME`),并在 `agentkit.yaml` 中设置一个真实的 `cr_instance_name`(或运行 `agentkit config`),然后在该目录内执行: + +```bash +cd my-harness +cp .env.example .env # 然后编辑它(设置 HARNESS_NAME) +# 编辑 agentkit.yaml:设置 cr_instance_name(或运行 `agentkit config`) +veadk harness deploy +``` + +| 选项 | 必填 | 说明 | +| :-- | :-- | :-- | +| `--volcengine-access-key` | 否 | 火山引擎 Access Key(默认读取 `VOLCENGINE_ACCESS_KEY` / `VOLC_ACCESSKEY`)。 | +| `--volcengine-secret-key` | 否 | 火山引擎 Secret Key(默认读取 `VOLCENGINE_SECRET_KEY` / `VOLC_SECRETKEY`)。 | +| `--region` | 否 | AgentKit 区域(默认 `cn-beijing` 或 `VOLCENGINE_REGION`)。 | +| `--role-name` | 否 | runtime IAM 角色名(默认自动生成,或读取 `HARNESS_RUNTIME_ROLE`)。 | +| `--path` | 否 | harness 目录,默认 `.`。 | + +`deploy` 会通过 AgentKit 的 hybrid 构建(本地 `docker build` + 推送到容器镜像仓库 CR)构建并推送 harness 镜像,然后调用 AgentKit 的 `CreateRuntime` 创建一个 runtime: + +- **Name** 取自 `.env` 中的 `HARNESS_NAME`(未设置时默认 `default`); +- **Tags** 携带一个内容为字面量 `Harness` 的标签(仅 Key,无 Value); +- **Envs** 取自该目录的 `.env`; +- **ArtifactType** 为 `image`,**ArtifactUrl** 为刚推送的镜像地址。 + +成功后会打印 runtime 的 name 与 id;可在 AgentKit 控制台或通过 AgentKit SDK(`GetRuntime`)查询其 Endpoint,再传给 `veadk agentkit harness invoke --url ...`。 + +## 客户端命令一览 + +服务部署完成后,`veadk agentkit harness` 是用于在运行时注册并调用 harness 的 HTTP 客户端。 | 命令 | 描述 | | :-- | :-- | diff --git a/veadk/cli/cli.py b/veadk/cli/cli.py index faa95c60..063b35c5 100644 --- a/veadk/cli/cli.py +++ b/veadk/cli/cli.py @@ -21,6 +21,7 @@ from veadk.cli.cli_deploy import deploy from veadk.cli.cli_eval import eval from veadk.cli.cli_frontend import frontend +from veadk.cli.cli_harness import harness from veadk.cli.cli_init import init from veadk.cli.cli_kb import kb from veadk.cli.cli_pipeline import pipeline @@ -59,6 +60,7 @@ def veadk(): veadk.add_command(clean) veadk.add_command(rl_group) veadk.add_command(agentkit) +veadk.add_command(harness) if __name__ == "__main__": veadk() diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py new file mode 100644 index 00000000..469e09de --- /dev/null +++ b/veadk/cli/cli_harness.py @@ -0,0 +1,487 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Top-level ``veadk harness`` command group. + +Two subcommands scaffold and deploy the harness server +(:mod:`veadk.cloud.harness_app`): + +* ``veadk harness create `` writes a deployable directory documenting every + harness environment variable in ``.env.example`` (plus a ``Dockerfile`` and an + ``agentkit.yaml`` preconfigured for an AgentKit runtime deploy). +* ``veadk harness deploy`` builds and pushes the harness Docker image via + AgentKit, then creates an **AgentKit runtime** from that image. + +This group is independent of ``veadk agentkit harness``, which is an HTTP client +for an *already deployed* server. +""" + +import json +import typing +from pathlib import Path + +import click + +from veadk.cloud.harness_app.types import HarnessOverrides + +# AgentKit runtime artifact type for a container image (defined in +# `agentkit/toolkit/runners/ve_agentkit.py::ARTIFACT_TYPE_DOCKER_IMAGE`). +_ARTIFACT_TYPE_IMAGE = "image" + +# Tag attached to every harness runtime so it can be discovered later. The +# locked requirement is a single tag whose key is the literal "Harness" with no +# value. +_HARNESS_TAG_KEY = "Harness" + +# Default harness/runtime name when `HARNESS_NAME` is unset in the `.env` +# (mirrors `veadk.cloud.harness_app.app.HARNESS_NAME`). +_DEFAULT_HARNESS_NAME = "default" + +# Documents every harness env var read by `veadk.cloud.harness_app.agent` +# (authoritative source: `harness_app/utils.py::_ENV_FIELDS` and the +# `agent.py` module docstring). Placeholder values are safe defaults; the +# server falls back to VeADK defaults for anything left unset. +_ENV_EXAMPLE = """\ +# --------------------------------------------------------------------------- +# Harness server environment variables. +# Copy to `.env` and fill in. Only set what you need; unset vars fall back to +# the VeADK defaults documented below. +# --------------------------------------------------------------------------- + +# --- Reasoning model ------------------------------------------------------- +# Model name. Unset uses the VeADK default model. +MODEL_AGENT_NAME=doubao-seed-1-6-250615 +# Model API credentials / endpoint (Volcengine Ark by default). +MODEL_AGENT_API_KEY=your-ark-api-key +MODEL_AGENT_API_BASE=https://ark.cn-beijing.volces.com/api/v3 +MODEL_AGENT_PROVIDER=openai + +# --- Agent definition ------------------------------------------------------ +# System prompt / instruction. Unset uses the VeADK default instruction. +SYSTEM_PROMPT=You are a helpful assistant. +# Comma-separated built-in tool names, e.g. "web_search,web_fetch". +TOOLS= +# Comma-separated skill hub names, e.g. "clawhub/lgwventrue/system-file-handler". +SKILLS= +# Agent runtime backend: "adk" (default) or "codex". +# "codex" requires the optional codex extra installed on the server. +RUNTIME=adk + +# --- Knowledge base & memory ---------------------------------------------- +# App/index name for the knowledge base and long-term memory. Default: harness_app. +APP_NAME=harness_app +# Knowledge base backend (e.g. "viking"). Leave empty to disable. +KNOWLEDGEBASE_TYPE= +# Long-term memory backend (e.g. "viking"). Leave empty to disable. +LONGTERM_MEM_TYPE= +# Short-term memory backend (e.g. "local", "mysql"). Default: local. +SHORTTERM_MEM_TYPE=local + +# --- Server ---------------------------------------------------------------- +# Logical harness name reported in invoke responses. Default: default. +HARNESS_NAME=default +# Skill hub download endpoint. Unset uses the public skill hub. +SKILL_HUB_DOWNLOAD_URL=https://skills.volces.com/v1/skills/download +# Bind host/port. On VeFaaS these are set automatically from the runtime port. +SERVER_HOST=0.0.0.0 +SERVER_PORT=8000 +""" + +# Container image entrypoint, mirroring `veadk/cloud/harness_app/Dockerfile` +# but importing the app from the installed package (no source files copied). +_DOCKERFILE = """\ +FROM python:3.12-slim + +WORKDIR /app + +# `[extensions]` pulls llama-index / redis / opensearch, required when the +# KNOWLEDGEBASE_TYPE or LONGTERM_MEM_TYPE env vars enable those components. +RUN apt-get update && apt-get install -y --no-install-recommends git && \\ + pip3 install --no-cache-dir \\ + "veadk-python[extensions] @ git+https://github.com/volcengine/veadk-python.git" && \\ + apt-get purge -y git && apt-get autoremove -y && \\ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# To run with the "codex" runtime (RUNTIME=codex), also install: +# pip3 install --no-cache-dir openai-codex + +EXPOSE 8000 + +CMD ["python", "-m", "uvicorn", "veadk.cloud.harness_app.app:app", \\ + "--host", "0.0.0.0", "--port", "8000"] +""" + +# AgentKit deploy config consumed by `agentkit.toolkit.sdk.build`. `launch_type: +# hybrid` builds the image locally with Docker and pushes it to Container +# Registry, which yields the pushed image URL `veadk harness deploy` feeds to +# CreateRuntime. The hand-written `Dockerfile` above has no AgentKit metadata +# header, so AgentKit keeps it as-is (KEEP_USER_CUSTOM) instead of regenerating. +# `ve_runtime_name` defaults to the harness name; `veadk harness deploy` +# overrides it from `HARNESS_NAME` in the `.env` anyway. +_AGENTKIT_YAML = """\ +# AgentKit build config for `veadk harness deploy`. `deploy` runs the hybrid +# build (local Docker build + push to Container Registry) and then creates an +# AgentKit runtime from the pushed image. Set a real `cr_instance_name` (or run +# `agentkit config`) so the image can be pushed; `Auto` skips the push. +common: + agent_name: harness + entry_point: app.py + description: VeADK harness server + language: Python + language_version: "3.12" + launch_type: hybrid +launch_types: + hybrid: + region: cn-beijing + ve_runtime_name: {runtime_name} + cr_instance_name: Auto + cr_namespace_name: agentkit + cr_repo_name: harness +""" + +_README = """\ +# Harness deployment directory + +This directory deploys the VeADK harness server +(`veadk.cloud.harness_app.app:app`) as a Volcengine **AgentKit runtime**. + +## Files + +- `.env.example` — every harness env var; copy to `.env` and fill in. +- `Dockerfile` — builds the image that serves the harness app. +- `agentkit.yaml` — AgentKit build config (hybrid: local build + push to + Container Registry). `ve_runtime_name` defaults to the harness name. +- `README.md` — this file. + +## Usage + +1. Copy `.env.example` to `.env` and fill in the values (model key, tools, + skills…). Set `HARNESS_NAME`; it becomes the runtime's name (defaults to + `default` when unset). +2. Set a real Container Registry instance in `agentkit.yaml` (`cr_instance_name`) + or run `agentkit config` — `Auto` skips the image push and the build cannot + produce an image URL. +3. From inside this directory, run: + + ```bash + veadk harness deploy + ``` + +`deploy` builds and pushes the Docker image via AgentKit, then creates an +AgentKit runtime named after `HARNESS_NAME` with a `Harness` tag. The env vars +in `.env` become the runtime's environment, so the cloud agent is assembled +exactly as configured here. +""" + +_CREATE_SUCCESS = """\ +Harness deployment directory created at {target}: +- .env.example (copy to .env and fill in) +- Dockerfile (builds the harness image) +- agentkit.yaml (AgentKit build config; set a real cr_instance_name) +- README.md + +Next steps: + cd {name} + cp .env.example .env # then edit it (set HARNESS_NAME) + # edit agentkit.yaml: set cr_instance_name (or run `agentkit config`) + veadk harness deploy +""" + + +@click.group() +def harness() -> None: + """Create and deploy a VeADK harness server.""" + pass + + +@harness.command("create") +@click.argument("dir_name") +def create(dir_name: str) -> None: + """Scaffold a deployable harness directory at DIR_NAME. + + Writes a `.env.example` documenting every harness environment variable, a + `Dockerfile` that builds the harness image, an `agentkit.yaml` build config, + and a short README. Copy `.env.example` to `.env`, fill it in, then run + `veadk harness deploy` from inside the directory. + """ + target = Path.cwd() / dir_name + if target.exists() and any(target.iterdir()): + click.confirm( + f"Directory '{target}' already exists and is not empty. Overwrite its files?", + abort=True, + ) + + target.mkdir(parents=True, exist_ok=True) + (target / ".env.example").write_text(_ENV_EXAMPLE) + (target / "Dockerfile").write_text(_DOCKERFILE) + (target / "agentkit.yaml").write_text( + _AGENTKIT_YAML.format(runtime_name=_DEFAULT_HARNESS_NAME) + ) + (target / "README.md").write_text(_README) + + click.secho(_CREATE_SUCCESS.format(target=target, name=dir_name), fg="green") + + +def _read_env_file(env_path: Path) -> dict[str, str]: + """Parse the harness directory's `.env` into a flat ``{KEY: VALUE}`` dict. + + Returns an empty dict when no `.env` exists. Used both to derive the runtime + name (`HARNESS_NAME`) and as the runtime's `Envs`. + """ + from dotenv import dotenv_values + + if not env_path.is_file(): + return {} + return {k: v for k, v in dotenv_values(env_path).items() if v is not None} + + +def _build_harness_image(proj_dir: Path) -> str: + """Build and push the harness image via AgentKit; return the pushed image URL. + + Reuses AgentKit's `sdk.build` (hybrid strategy: local Docker build + push to + Container Registry). The pushed image URL is read from the build result's + `metadata["cr_image_url"]` (the push step records it there), falling back to + the local image's full name. Fast-fails when the build did not push an image. + """ + from agentkit.toolkit import sdk + from agentkit.toolkit.reporter import LoggingReporter + + config_file = proj_dir / "agentkit.yaml" + if not config_file.is_file(): + raise click.ClickException( + f"No `agentkit.yaml` in '{proj_dir}'. Run `veadk harness create` first." + ) + + result = sdk.build(config_file=str(config_file), reporter=LoggingReporter()) + if not result.success: + raise click.ClickException(f"Harness image build failed: {result.error}") + + image_url = (result.metadata or {}).get("cr_image_url") + if not image_url: + raise click.ClickException( + "Build succeeded but no image was pushed to Container Registry. Set a " + "real `cr_instance_name` in `agentkit.yaml` (or run `agentkit config`)." + ) + return image_url + + +def _create_harness_runtime( + *, + runtime_name: str, + role_name: str, + image_url: str, + envs: dict[str, str], + region: str, +) -> str: + """Create an AgentKit runtime for the harness image; return its runtime id. + + Ensures the IAM role exists (CreateRuntime requires it), then issues + CreateRuntime with the harness name, a `Harness` tag, the `.env` as `Envs`, + and the pushed image as an `image` artifact. + """ + from agentkit.sdk.runtime import types as rt + from agentkit.sdk.runtime.client import AgentkitRuntimeClient + from agentkit.toolkit.volcengine.iam import VeIAM + from agentkit.utils.misc import generate_apikey_name, generate_client_token + + if not VeIAM(region=region).ensure_role_for_agentkit(role_name): + raise click.ClickException( + f"Failed to create or ensure the runtime IAM role `{role_name}`." + ) + + client = AgentkitRuntimeClient(region=region) + # The runtime types use by-alias (PascalCase) fields with `populate_by_name`; + # build the request from an alias-keyed dict via `model_validate` so static + # type checking matches the API field names exactly. + request = rt.CreateRuntimeRequest.model_validate( + { + "Name": runtime_name, + "RoleName": role_name, + "ArtifactType": _ARTIFACT_TYPE_IMAGE, + "ArtifactUrl": image_url, + "Envs": [{"Key": k, "Value": v} for k, v in envs.items()], + "Tags": [{"Key": _HARNESS_TAG_KEY}], + "AuthorizerConfiguration": { + "KeyAuth": { + "ApiKeyName": generate_apikey_name(), + "ApiKeyLocation": "HEADER", + } + }, + "ClientToken": generate_client_token(), + } + ) + response = client.create_runtime(request) + return response.runtime_id or "" + + +@harness.command("deploy") +@click.option("--volcengine-access-key", default=None, help="Volcengine access key.") +@click.option("--volcengine-secret-key", default=None, help="Volcengine secret key.") +@click.option( + "--region", + default=None, + help="AgentKit region (default `cn-beijing` or VOLCENGINE_REGION).", +) +@click.option( + "--role-name", + default=None, + help="Runtime IAM role name (default: auto-generated or HARNESS_RUNTIME_ROLE).", +) +@click.option( + "--path", + default=".", + help="Harness directory (created by `veadk harness create`).", +) +def deploy( + volcengine_access_key: str | None, + volcengine_secret_key: str | None, + region: str | None, + role_name: str | None, + path: str, +) -> None: + """Build the harness image and deploy it as an AgentKit runtime. + + Run this from inside a directory created by `veadk harness create` (with a + filled-in `.env` and an `agentkit.yaml`). It builds and pushes the harness + Docker image via AgentKit, then creates an AgentKit runtime whose: + + * Name is the harness name (`HARNESS_NAME` in the `.env`, default `default`), + * Envs are the directory's `.env`, + * Tags carry a single `Harness` tag, + * artifact is the pushed image. + """ + import os + + from veadk.utils.logger import get_logger + + logger = get_logger(__name__) + + proj_dir = Path(path).resolve() + if not proj_dir.is_dir(): + raise click.ClickException(f"Path '{proj_dir}' is not a directory.") + + # AgentKit's build/runtime clients authenticate via the Volcengine SDK, which + # reads VOLC_ACCESSKEY / VOLC_SECRETKEY from the environment. Mirror whatever + # AK/SK was passed (or already set as VOLCENGINE_*) into those names. + access_key = volcengine_access_key or os.getenv("VOLCENGINE_ACCESS_KEY", "") + secret_key = volcengine_secret_key or os.getenv("VOLCENGINE_SECRET_KEY", "") + if access_key and secret_key: + os.environ["VOLC_ACCESSKEY"] = access_key + os.environ["VOLC_SECRETKEY"] = secret_key + if not os.getenv("VOLC_ACCESSKEY") or not os.getenv("VOLC_SECRETKEY"): + raise click.ClickException( + "Volcengine credentials are required. Pass --volcengine-access-key / " + "--volcengine-secret-key, or set VOLCENGINE_ACCESS_KEY / " + "VOLCENGINE_SECRET_KEY (or VOLC_ACCESSKEY / VOLC_SECRETKEY)." + ) + + envs = _read_env_file(proj_dir / ".env") + runtime_name = envs.get("HARNESS_NAME") or _DEFAULT_HARNESS_NAME + resolved_region = region or os.getenv("VOLCENGINE_REGION") or "cn-beijing" + resolved_role = role_name or os.getenv("HARNESS_RUNTIME_ROLE") + if not resolved_role: + from agentkit.utils.misc import generate_runtime_role_name + + resolved_role = generate_runtime_role_name() + + logger.info(f"Building harness image from {proj_dir}") + image_url = _build_harness_image(proj_dir) + logger.info(f"Built harness image: {image_url}") + + runtime_id = _create_harness_runtime( + runtime_name=runtime_name, + role_name=resolved_role, + image_url=image_url, + envs=envs, + region=resolved_region, + ) + + click.secho( + f"Harness runtime created: name={runtime_name} id={runtime_id}\n" + f"Image: {image_url}\n" + f"Tagged `{_HARNESS_TAG_KEY}`. Find its endpoint in the AgentKit console " + "or via the AgentKit SDK (GetRuntime).", + fg="green", + ) + + +def _override_options(func): + """Attach a ``--flag`` for every :class:`HarnessOverrides` field. + + The override flags are generated from the model, so adding a field to + ``HarnessOverrides`` exposes a new CLI flag automatically — there is no second + place to update. Each flag defaults to ``None`` (unset → omitted from the + request), preserving the server's partial-override semantics. + """ + for name, field in reversed(list(HarnessOverrides.model_fields.items())): + option: dict = { + "default": None, + "help": field.description or f"Override `{name}` for this call.", + } + if typing.get_origin(field.annotation) is typing.Literal: + option["type"] = click.Choice( + [str(arg) for arg in typing.get_args(field.annotation)] + ) + func = click.option("--" + name.replace("_", "-"), name, **option)(func) + return func + + +@harness.command("invoke") +@click.argument("message") +@click.option( + "--harness", "harness_name", required=True, help="Harness name to invoke." +) +@click.option( + "--user-id", "user_id", default="cli-user", help="User id for the session." +) +@click.option( + "--session-id", + "session_id", + default="cli-session", + help="Session id for the call.", +) +@click.option( + "--url", + required=True, + envvar="HARNESS_URL", + help="Harness server base URL (or set HARNESS_URL).", +) +@click.option( + "--key", + default=None, + envvar="HARNESS_KEY", + help="Gateway API key for Bearer auth (or set HARNESS_KEY).", +) +@_override_options +def invoke(message, harness_name, user_id, session_id, url, key, **overrides) -> None: + """Invoke a deployed harness with MESSAGE and print its output. + + Any override flag (generated from ``HarnessOverrides``) applies a once-time + override on top of the deployed agent for this single call; unset flags are + omitted, so the server keeps its configured values (memory and the knowledge + base are never overridable). + """ + from veadk.cli.cli_agentkit import _harness_request + + body: dict = { + "prompt": message, + "harness_name": harness_name, + "run_agent_request": {"user_id": user_id, "session_id": session_id}, + } + override = {name: value for name, value in overrides.items() if value is not None} + if override: + body["harness"] = override + + result = _harness_request(url, "/harness/invoke", key, body) + click.echo(result.get("output", json.dumps(result, ensure_ascii=False))) diff --git a/veadk/cloud/harness_app.py b/veadk/cloud/harness_app.py deleted file mode 100644 index 58a97e6b..00000000 --- a/veadk/cloud/harness_app.py +++ /dev/null @@ -1,267 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import io -import os -import shutil -import zipfile -from pathlib import Path -from typing import Literal - -import frontmatter -import httpx -from fastapi import FastAPI -from google.adk.skills import load_skill_from_dir -from google.adk.tools.skill_toolset import SkillToolset -from pydantic import BaseModel, Field - -from veadk import Agent -from veadk.consts import DEFAULT_MODEL_AGENT_NAME -from veadk.memory.short_term_memory import ShortTermMemory -from veadk.runner import Runner -from veadk.utils.logger import get_logger -from veadk.utils.misc import formatted_timestamp - -logger = get_logger(__name__) - -# Skill hub download endpoint. A skill name in a harness is the path after -# `/download/`, e.g. "clawhub/lgwventrue/system-file-handler". -SKILL_HUB_DOWNLOAD_URL = os.getenv( - "SKILL_HUB_DOWNLOAD_URL", "https://skills.volces.com/v1/skills/download" -) - - -def _split_csv(value: str) -> list[str]: - """Split a comma-separated string into a list of trimmed, non-empty names. - - ``"web_search, web_fetch"`` -> ``["web_search", "web_fetch"]``; ``""`` -> ``[]``. - """ - return [item.strip() for item in value.split(",") if item.strip()] - - -def _download_and_extract_skill(skill: str, dest_dir: Path) -> Path: - """Download a skill zip from the skill hub and extract it. - - Args: - skill: Skill identifier — the hub path after ``/download/`` - (e.g. ``"clawhub/lgwventrue/system-file-handler"``). - dest_dir: Base directory to extract into; the skill is placed in a - subdirectory named after the identifier's last path segment. - - Returns: - The directory the skill was extracted to. Its name matches the skill's - declared name in ``SKILL.md`` (required by ``load_skill_from_dir``). - """ - name = skill.strip("/") - url = f"{SKILL_HUB_DOWNLOAD_URL.rstrip('/')}/{name}" - logger.info(f"Downloading skill '{skill}' from {url}") - - response = httpx.get(url, timeout=60, follow_redirects=True) - if response.status_code != 200: - raise RuntimeError( - f"Failed to download skill '{skill}': HTTP {response.status_code}" - ) - - # Extract to a staging dir first; the final directory must be named after - # the skill's declared name (ADK's load_skill_from_dir enforces this). - staging = dest_dir / f"{name.split('/')[-1]}__staging" - if staging.exists(): - shutil.rmtree(staging) - staging.mkdir(parents=True) - staging_root = staging.resolve() - with zipfile.ZipFile(io.BytesIO(response.content)) as zf: - for member in zf.namelist(): - # Guard against path traversal (zip-slip). - if not (staging / member).resolve().is_relative_to(staging_root): - raise RuntimeError(f"Unsafe path in skill '{skill}' zip: {member}") - zf.extractall(staging) - - skill_md = staging / "SKILL.md" - if not skill_md.exists(): - skill_md = staging / "skill.md" - if not skill_md.exists(): - raise RuntimeError(f"Skill '{skill}' has no SKILL.md") - declared_name = frontmatter.loads( - skill_md.read_text(encoding="utf-8") - ).metadata.get("name") - if not declared_name: - raise RuntimeError(f"Skill '{skill}' SKILL.md has no 'name' in frontmatter") - - skill_dir = dest_dir / str(declared_name) - if skill_dir.exists(): - shutil.rmtree(skill_dir) - staging.rename(skill_dir) - - logger.info(f"Extracted skill '{skill}' (name='{declared_name}') to {skill_dir}") - return skill_dir - - -class Harness(BaseModel): - model_name: str = Field(default=DEFAULT_MODEL_AGENT_NAME) - # `tools` and `skills` are comma-separated strings (e.g. "web_search,web_fetch"). - # The app splits them into names via _split_csv(); clients (CLI/curl) just - # pass the raw string. - tools: str = Field(default="") - skills: str = Field(default="") - system_prompt: str = Field(default="You are a helpful assistant.") - # Agent runtime backend: "adk" (default) or "codex". Passed through to the - # Agent; "codex" requires the optional codex extra on the server. - runtime: Literal["adk", "codex"] = Field(default="adk") - - -class AddHarnessRequest(BaseModel): - harness_name: str - harness: Harness - - -class AddHarnessResponse(BaseModel): - code: int = Field(default=200) - msg: str = Field(default="Harness added successfully.") - harness_name: str - - -class RunAgentRequest(BaseModel): - user_id: str - session_id: str - - -class InvokeHarnessRequest(BaseModel): - prompt: str - harness_name: str - harness: Harness | None = None - run_agent_request: RunAgentRequest - - -class InvokeHarnessResponse(BaseModel): - harness_name: str - overwrite: bool = Field( - default=False - ) # Whether the agent is created with once-time harness or not. - output: str - - -class HarnessApp: - def __init__(self): - self.app = FastAPI() - self.agents = {} - - self.short_term_memory = ShortTermMemory(backend="local") - - self.mount() - - def mount(self): - @self.app.post("/harness/add") - def add_harness(request: AddHarnessRequest) -> AddHarnessResponse: - if request.harness_name in self.agents: - logger.warning( - f"Harness with name {request.harness_name} already exists." - ) - return AddHarnessResponse( - code=400, - msg=f"Harness with name {request.harness_name} already exists.", - harness_name=request.harness_name, - ) - - agent = self._create_agent(request.harness) - self.agents[request.harness_name] = agent - return AddHarnessResponse(harness_name=request.harness_name) - - @self.app.post("/harness/invoke") - async def invoke_harness( - request: InvokeHarnessRequest, - ) -> InvokeHarnessResponse: - if request.harness_name not in self.agents: - logger.error( - f"Harness with name {request.harness_name} does not exist." - ) - return InvokeHarnessResponse( - harness_name=request.harness_name, - output=f"Harness with name {request.harness_name} does not exist. Please add it first.", - ) - - if request.harness: - logger.info( - f"Temporarily create agent with once-time harness {request.harness}." - ) - agent = self._create_agent(request.harness) - else: - agent = self.agents[request.harness_name] - - agent_runner = Runner( - agent=agent, - short_term_memory=self.short_term_memory, - app_name=request.harness_name, - ) - output = await agent_runner.run( - messages=[request.prompt], - user_id=request.run_agent_request.user_id, - session_id=request.run_agent_request.session_id, - ) - - return InvokeHarnessResponse( - harness_name=request.harness_name, - overwrite=request.harness is not None, - output=output, - ) - - def _create_skill_toolset(self, skills: list[str]) -> SkillToolset | None: - # Pull each skill zip from the hub into a fresh /tmp/ dir, - # extract it, and load it as an ADK skill. Skills that fail to download - # or load (e.g. a malformed SKILL.md name) are skipped with a warning so - # the rest still load. Returns None if none loaded. - base_dir = Path("/tmp") / formatted_timestamp() - loaded_skills = [] - for skill in skills: - try: - loaded_skills.append( - load_skill_from_dir(_download_and_extract_skill(skill, base_dir)) - ) - except Exception as e: - logger.warning(f"Skipping skill '{skill}': {e}") - - if not loaded_skills: - logger.warning("No skills loaded successfully; skipping skill toolset.") - return None - return SkillToolset(skills=loaded_skills) - - def _create_agent(self, harness: Harness) -> Agent: - from veadk.tools import get_builtin_tool - - tools = [get_builtin_tool(name) for name in _split_csv(harness.tools)] - skills = _split_csv(harness.skills) - if skills: - logger.info(f"Loading skills {skills} for harness.") - skill_toolset = self._create_skill_toolset(skills) - if skill_toolset is not None: - tools = tools + [skill_toolset] - - agent = Agent( - name="temp_agent", - model_name=harness.model_name, - instruction=harness.system_prompt, - tools=tools, - runtime=harness.runtime, - ) - return agent - - def serve(self, host: str = "0.0.0.0", port: int = 8000) -> None: - import uvicorn - - uvicorn.run(self.app, host=host, port=port) - - -if __name__ == "__main__": - # Entry for `python -m veadk.cloud.harness_app` (e.g. the AgentKit runtime), - # serving the API on 0.0.0.0:8000. - HarnessApp().serve() diff --git a/veadk/cloud/harness_app/Dockerfile b/veadk/cloud/harness_app/Dockerfile new file mode 100644 index 00000000..07445d79 --- /dev/null +++ b/veadk/cloud/harness_app/Dockerfile @@ -0,0 +1,34 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.12-slim + +WORKDIR /app + +# `[extensions]` pulls llama-index / redis / opensearch, required when the +# KNOWLEDGEBASE_TYPE or LONGTERM_MEM_TYPE env vars enable those components. +# (Viking / MySQL / PostgreSQL backends are already in the base dependencies.) +RUN apt-get update && apt-get install -y --no-install-recommends git && \ + pip3 install --no-cache-dir "veadk-python[extensions] @ git+https://github.com/volcengine/veadk-python.git" && \ + apt-get purge -y git && apt-get autoremove -y && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# To run with the "codex" runtime (RUNTIME=codex), also install: +# pip3 install --no-cache-dir openai-codex + +COPY agent.py app.py ./ + +EXPOSE 8000 + +CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/veadk/cloud/harness_app/__init__.py b/veadk/cloud/harness_app/__init__.py new file mode 100644 index 00000000..7f463206 --- /dev/null +++ b/veadk/cloud/harness_app/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/veadk/cloud/harness_app/agent.py b/veadk/cloud/harness_app/agent.py new file mode 100644 index 00000000..f78a3d96 --- /dev/null +++ b/veadk/cloud/harness_app/agent.py @@ -0,0 +1,37 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Assemble a single VeADK agent for the harness server from the environment. + +The agent is built once at import time from environment variables (parsed into a +:class:`HarnessConfig`; see ``types.py`` and ``utils.py``). The knowledge base and +long-term memory are only created when their backend is set; the short-term +memory backend defaults to ``local``. + +Environment variables: + MODEL_AGENT_NAME Reasoning model name. Default: VeADK default model. + SYSTEM_PROMPT Agent instruction. Default: VeADK default instruction. + TOOLS Comma-separated built-in tool names, e.g. "web_search,link_reader". + SKILLS Comma-separated skill names, e.g. "data-visualization-cloud,...". + RUNTIME Agent runtime backend: "adk" (default) or "codex". + APP_NAME App/index name for the knowledge base and long-term memory. + Default: "harness_app". + KNOWLEDGEBASE_TYPE Knowledge base backend (e.g. "viking"). Unset disables it. + LONGTERM_MEM_TYPE Long-term memory backend (e.g. "viking"). Unset disables it. + SHORTTERM_MEM_TYPE Short-term memory backend (e.g. "sqlite"). Default: "local". +""" + +from veadk.cloud.harness_app.utils import init_harness_agent + +agent, short_term_memory = init_harness_agent() diff --git a/veadk/cloud/harness_app/app.py b/veadk/cloud/harness_app/app.py new file mode 100644 index 00000000..1c936bb9 --- /dev/null +++ b/veadk/cloud/harness_app/app.py @@ -0,0 +1,120 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Harness server: serve the env-assembled agent over HTTP. + +The agent is built once from the environment (see ``agent.py``) and served at +``POST /harness/invoke``. A request may carry a once-time ``harness`` override: +the base agent is cloned, the override applied, and a throwaway runner drives +that clone for the single call. + +Run with either: + python app.py + uvicorn app:app --host 0.0.0.0 --port 8000 +""" + +import os +import tempfile +from pathlib import Path + +from fastapi import FastAPI + +from veadk import Agent +from veadk.cloud.harness_app.agent import agent, short_term_memory +from veadk.cloud.harness_app.types import ( + InvokeHarnessRequest, + InvokeHarnessResponse, +) +from veadk.cloud.harness_app.utils import spawn_harness_agent +from veadk.memory.short_term_memory import ShortTermMemory +from veadk.runner import Runner +from veadk.utils.logger import get_logger + +logger = get_logger(__name__) + +HARNESS_NAME = os.getenv("HARNESS_NAME", "default") + + +class HarnessApp: + def __init__( + self, + agent: Agent, + short_term_memory: ShortTermMemory, + harness_name: str = "default", + ): + self.app = FastAPI() + self.agent = agent + self.short_term_memory = short_term_memory + self.harness_name = harness_name + self.runner = Runner( + agent=agent, + short_term_memory=short_term_memory, + app_name=harness_name, + ) + + self.mount() + + def mount(self): + @self.app.post("/harness/invoke") + async def invoke_harness( + request: InvokeHarnessRequest, + ) -> InvokeHarnessResponse: + if request.harness is not None: + logger.info(f"Applying once-time harness override: {request.harness}") + # The override clones the base agent and may download incremental + # skills into a temp dir; the skill files are read from disk while + # the agent runs, so the dir is removed (and the one-off agent + + # runner dropped) only after the run finishes. + with tempfile.TemporaryDirectory(prefix="harness_invoke_") as work_dir: + agent = spawn_harness_agent( + self.agent, request.harness, download_dir=Path(work_dir) + ) + runner = Runner( + agent=agent, + short_term_memory=self.short_term_memory, + app_name=self.harness_name, + ) + output = await runner.run( + messages=[request.prompt], + user_id=request.run_agent_request.user_id, + session_id=request.run_agent_request.session_id, + ) + else: + output = await self.runner.run( + messages=[request.prompt], + user_id=request.run_agent_request.user_id, + session_id=request.run_agent_request.session_id, + ) + + return InvokeHarnessResponse( + harness_name=self.harness_name, + overwrite=request.harness is not None, + output=output, + ) + + def serve(self, host: str = "0.0.0.0", port: int = 8000) -> None: + import uvicorn + + uvicorn.run(self.app, host=host, port=port) + + +harness_app = HarnessApp(agent, short_term_memory, HARNESS_NAME) +app = harness_app.app + + +if __name__ == "__main__": + harness_app.serve( + host=os.getenv("SERVER_HOST", "0.0.0.0"), + port=int(os.getenv("SERVER_PORT", "8000")), + ) diff --git a/veadk/cloud/harness_app/types.py b/veadk/cloud/harness_app/types.py new file mode 100644 index 00000000..7a874265 --- /dev/null +++ b/veadk/cloud/harness_app/types.py @@ -0,0 +1,97 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Harness parameter schemas for the deployable harness app. + +The parameters split into two groups: + +* :class:`HarnessOverrides` — the subset that may be overridden per invocation + (model, prompt, tools, skills, runtime). +* :class:`HarnessConfig` — the full set fixed at agent creation time. It extends + the overridable params with the knowledge base and memory components, which are + bound when the agent is built and therefore **cannot** be overridden per request. + +``tools`` and ``skills`` are comma-separated strings (e.g. ``"web_search,web_fetch"``). +""" + +from typing import Literal + +from pydantic import BaseModel, Field + +from veadk.consts import DEFAULT_MODEL_AGENT_NAME +from veadk.prompts.agent_default_prompt import DEFAULT_INSTRUCTION + + +class HarnessOverrides(BaseModel): + """Harness parameters that may be overridden on a per-invocation basis. + + Field descriptions are the single source of truth for both the FastAPI schema + and the ``veadk harness invoke`` CLI flags (which are generated from these + fields), so adding a field here exposes a new override everywhere. + """ + + model_name: str = Field( + default=DEFAULT_MODEL_AGENT_NAME, description="Reasoning model name." + ) + tools: str = Field( + default="", + description="Comma-separated built-in tool names, e.g. web_search,web_fetch.", + ) + skills: str = Field(default="", description="Comma-separated skill hub names.") + system_prompt: str = Field( + default="You are a helpful assistant.", + description="System prompt / instruction.", + ) + runtime: Literal["adk", "codex"] = Field( + default="adk", description="Agent runtime backend." + ) + + +class HarnessConfig(HarnessOverrides): + """Full harness parameters fixed when the agent is created. + + Extends :class:`HarnessOverrides` with the knowledge base and memory + backends. These are wired into the agent at build time and cannot be changed + per request, so they are intentionally absent from :class:`HarnessOverrides`. + + An empty backend string means the component is disabled (not created). + """ + + app_name: str = Field(default="harness_app", alias="name") + system_prompt: str = Field(default=DEFAULT_INSTRUCTION) + knowledgebase_type: str = Field(default="") + longterm_memory_type: str = Field(default="") + shortterm_memory_type: str = Field(default="local") + runtime: Literal["adk", "codex"] = Field(default="adk") + + +class RunAgentRequest(BaseModel): + user_id: str + session_id: str + + +class InvokeHarnessRequest(BaseModel): + prompt: str + harness_name: str + # When present, a once-time override applied on top of the served agent for + # this single call. Only the fields actually set are applied; memory and the + # knowledge base are never overridable (absent from HarnessOverrides). + harness: HarnessOverrides | None = None + run_agent_request: RunAgentRequest + + +class InvokeHarnessResponse(BaseModel): + harness_name: str + overwrite: bool = Field(default=False) + output: str diff --git a/veadk/cloud/harness_app/utils.py b/veadk/cloud/harness_app/utils.py new file mode 100644 index 00000000..24717e9c --- /dev/null +++ b/veadk/cloud/harness_app/utils.py @@ -0,0 +1,341 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for assembling the harness agent. + +Two factory functions cover the two creation paths: + +* :func:`init_harness_agent` — first-time startup; reads the environment into a + :class:`HarnessConfig` and builds the long-lived agent, downloading its skills + from the skill hub and mounting them as an ADK skill toolset. +* :func:`spawn_harness_agent` — temporary, one-off creation that clones the base + agent and applies a per-request override (incremental tools/skills on top). +""" + +import io +import os +import shutil +import tempfile +import zipfile +from pathlib import Path +from typing import Any + +import frontmatter +import httpx +from google.adk.skills import load_skill_from_dir +from google.adk.tools.skill_toolset import SkillToolset + +from veadk import Agent +from veadk.cloud.harness_app.types import HarnessConfig, HarnessOverrides +from veadk.knowledgebase import KnowledgeBase +from veadk.memory.long_term_memory import LongTermMemory +from veadk.memory.short_term_memory import ShortTermMemory +from veadk.tools import get_builtin_tool +from veadk.utils.logger import get_logger + +logger = get_logger(__name__) + +__all__ = [ + "HarnessConfig", + "HarnessOverrides", + "split_csv", + "build_skill_toolset", + "config_from_env", + "init_harness_agent", + "spawn_harness_agent", +] + +# Skill hub download endpoint. A skill name in a harness is the path after +# `/download/`, e.g. "clawhub/lgwventrue/system-file-handler". +SKILL_HUB_DOWNLOAD_URL = os.getenv( + "SKILL_HUB_DOWNLOAD_URL", "https://skills.volces.com/v1/skills/download" +) + +# Maps HarnessConfig field names to their environment variables. ``app_name`` is +# populated via its "name" alias. Only variables that are set are passed, so the +# model's own defaults apply to everything else. +_ENV_FIELDS = { + "model_name": "MODEL_AGENT_NAME", + "tools": "TOOLS", + "skills": "SKILLS", + "system_prompt": "SYSTEM_PROMPT", + "runtime": "RUNTIME", + "name": "APP_NAME", + "knowledgebase_type": "KNOWLEDGEBASE_TYPE", + "longterm_memory_type": "LONGTERM_MEM_TYPE", + "shortterm_memory_type": "SHORTTERM_MEM_TYPE", +} + + +def split_csv(value: str) -> list[str]: + """Split a comma-separated string into trimmed, non-empty names. + + ``"web_search, web_fetch"`` -> ``["web_search", "web_fetch"]``; ``""`` -> ``[]``. + """ + return [item.strip() for item in value.split(",") if item.strip()] + + +def _download_and_extract_skill(skill: str, dest_dir: Path) -> Path: + """Download a skill zip from the skill hub and extract it. + + Args: + skill: Skill identifier — the hub path after ``/download/`` + (e.g. ``"clawhub/lgwventrue/system-file-handler"``). + dest_dir: Base directory to extract into; the skill is placed in a + subdirectory named after its declared name in ``SKILL.md``. + + Returns: + The directory the skill was extracted to. Its name matches the skill's + declared name in ``SKILL.md`` (required by ``load_skill_from_dir``). + """ + name = skill.strip("/") + url = f"{SKILL_HUB_DOWNLOAD_URL.rstrip('/')}/{name}" + logger.info(f"Downloading skill '{skill}' from {url}") + + response = httpx.get(url, timeout=60, follow_redirects=True) + if response.status_code != 200: + raise RuntimeError( + f"Failed to download skill '{skill}': HTTP {response.status_code}" + ) + + # Extract to a staging dir first; the final directory must be named after + # the skill's declared name (ADK's load_skill_from_dir enforces this). + staging = dest_dir / f"{name.split('/')[-1]}__staging" + if staging.exists(): + shutil.rmtree(staging) + staging.mkdir(parents=True) + staging_root = staging.resolve() + with zipfile.ZipFile(io.BytesIO(response.content)) as zf: + for member in zf.namelist(): + # Guard against path traversal (zip-slip). + if not (staging / member).resolve().is_relative_to(staging_root): + raise RuntimeError(f"Unsafe path in skill '{skill}' zip: {member}") + zf.extractall(staging) + + skill_md = staging / "SKILL.md" + if not skill_md.exists(): + skill_md = staging / "skill.md" + if not skill_md.exists(): + raise RuntimeError(f"Skill '{skill}' has no SKILL.md") + declared_name = frontmatter.loads( + skill_md.read_text(encoding="utf-8") + ).metadata.get("name") + if not declared_name: + raise RuntimeError(f"Skill '{skill}' SKILL.md has no 'name' in frontmatter") + + skill_dir = dest_dir / str(declared_name) + if skill_dir.exists(): + shutil.rmtree(skill_dir) + staging.rename(skill_dir) + + logger.info(f"Extracted skill '{skill}' (name='{declared_name}') to {skill_dir}") + return skill_dir + + +def build_skill_toolset( + skills: list[str], download_dir: Path | None = None +) -> SkillToolset | None: + """Download each skill from the hub and load them as a single ADK toolset. + + Skills are downloaded into ``download_dir`` (a fresh temp dir when omitted) + and loaded via ``load_skill_from_dir``. The directory is **not** cleaned up + here: a skill's scripts/assets are read from disk while the agent runs, so + the caller owns the directory's lifetime (the base agent keeps its skills for + the server's lifetime; a per-invoke override cleans up after the run). Skills + that fail to download or load (e.g. a malformed ``SKILL.md``) are skipped with + a warning so the rest still load. + + Returns: + A :class:`SkillToolset` of the loaded skills, or ``None`` if none loaded. + """ + if download_dir is None: + download_dir = Path(tempfile.mkdtemp(prefix="harness_skills_")) + loaded_skills = [] + for skill in skills: + try: + loaded_skills.append( + load_skill_from_dir(_download_and_extract_skill(skill, download_dir)) + ) + except Exception as e: + logger.warning(f"Skipping skill '{skill}': {e}") + + if not loaded_skills: + logger.warning("No skills loaded successfully; skipping skill toolset.") + return None + return SkillToolset(skills=loaded_skills) + + +def config_from_env() -> HarnessConfig: + """Parse the environment into a :class:`HarnessConfig` (validated by pydantic).""" + kwargs: dict[str, Any] = { + field: os.environ[env] + for field, env in _ENV_FIELDS.items() + if env in os.environ + } + return HarnessConfig(**kwargs) + + +def _assemble_agent(config: HarnessConfig) -> tuple[Agent, ShortTermMemory]: + """Build an agent and its short-term memory from a :class:`HarnessConfig`. + + Skills are downloaded from the skill hub and mounted as an ADK + :class:`SkillToolset` tool. An empty backend string disables the knowledge + base / long-term memory. Backend values are validated by each component's + pydantic model (fast-fail on an unknown value). + """ + tools = [get_builtin_tool(name) for name in split_csv(config.tools)] + + skills = split_csv(config.skills) + if skills: + logger.info(f"Loading skills {skills} for harness.") + skill_toolset = build_skill_toolset(skills) + if skill_toolset is not None: + tools.append(skill_toolset) + + knowledgebase = None + if config.knowledgebase_type: + logger.info( + f"Initializing knowledge base: backend={config.knowledgebase_type} " + f"index={config.app_name}" + ) + knowledgebase = KnowledgeBase( + backend=config.knowledgebase_type, # type: ignore[arg-type] + app_name=config.app_name, + ) + + long_term_memory = None + if config.longterm_memory_type: + logger.info( + f"Initializing long-term memory: backend={config.longterm_memory_type} " + f"index={config.app_name}" + ) + long_term_memory = LongTermMemory( + backend=config.longterm_memory_type, # type: ignore[arg-type] + app_name=config.app_name, + ) + + logger.info( + f"Initializing short-term memory: backend={config.shortterm_memory_type}" + ) + short_term_memory = ShortTermMemory( + backend=config.shortterm_memory_type # type: ignore[arg-type] + ) + + agent = Agent( + name="harness_agent", + model_name=config.model_name, + instruction=config.system_prompt, + tools=tools, + runtime=config.runtime, + knowledgebase=knowledgebase, + long_term_memory=long_term_memory, + short_term_memory=short_term_memory, + ) + return agent, short_term_memory + + +def init_harness_agent() -> tuple[Agent, ShortTermMemory]: + """Create the long-lived agent on first startup by reading the environment. + + Returns: + A ``(agent, short_term_memory)`` tuple. The short-term memory is returned + separately so the server can share the same instance with its ``Runner``. + """ + return _assemble_agent(config_from_env()) + + +def _tool_name(tool: Any) -> str | None: + """The dispatch name of a tool (function ``__name__`` or tool/toolset ``name``).""" + return getattr(tool, "__name__", None) or getattr(tool, "name", None) + + +def _add_incremental_tools(agent: Agent, tool_names: list[str]) -> None: + """Append the requested built-in tools, skipping ones already on the agent.""" + existing = {name for tool in agent.tools if (name := _tool_name(tool))} + for name in tool_names: + if name in existing: + logger.info(f"Tool '{name}' already on the agent; skipping.") + continue + agent.tools.append(get_builtin_tool(name)) + existing.add(name) + + +def _add_incremental_skills( + agent: Agent, skill_ids: list[str], download_dir: Path | None = None +) -> None: + """Mount the requested skills, skipping ones whose name is already loaded. + + Skills already present are dropped (deduped by skill name). Any genuinely new + skills are merged into the agent's existing :class:`SkillToolset` so the agent + keeps a single toolset (two would expose duplicate ``list_skills``/``load_skill`` + tools); if the agent has none yet, a new toolset is mounted. ``download_dir`` + is where the skills are downloaded (cleaned up by the caller after the run). + """ + toolset = build_skill_toolset(skill_ids, download_dir=download_dir) + if toolset is None: + return + new_skills = toolset._list_skills() + + existing_toolset = next( + (tool for tool in agent.tools if isinstance(tool, SkillToolset)), None + ) + if existing_toolset is None: + agent.tools.append(toolset) + return + + existing_skills = existing_toolset._list_skills() + existing_names = {skill.name for skill in existing_skills} + new_skills = [skill for skill in new_skills if skill.name not in existing_names] + if not new_skills: + logger.info("All requested skills already loaded; skipping.") + return + + agent.tools.remove(existing_toolset) + agent.tools.append(SkillToolset(skills=existing_skills + new_skills)) + + +def spawn_harness_agent( + base_agent: Agent, overrides: HarnessOverrides, download_dir: Path | None = None +) -> Agent: + """Clone the base agent for a one-off invocation and apply per-request overrides. + + Uses ADK's :meth:`~google.adk.agents.base_agent.BaseAgent.clone`, so the clone + inherits the base agent's knowledge base and memory — these are never + overridable. Only the fields the request actually set are applied: ``model_name``, + ``system_prompt`` and ``runtime`` replace the base value, while ``tools`` and + ``skills`` are mounted *incrementally* — anything already on the agent (same + tool name / skill name) is skipped, so only the delta is added. + + ``download_dir`` is where any incremental skills are downloaded; the caller + owns it and should remove it once the invocation finishes. + """ + set_fields = overrides.model_fields_set + + update: dict[str, Any] = {} + if "system_prompt" in set_fields: + update["instruction"] = overrides.system_prompt + if "runtime" in set_fields: + update["runtime"] = overrides.runtime + cloned = base_agent.clone(update=update) + + if "model_name" in set_fields: + cloned.update_model(overrides.model_name) + + if "tools" in set_fields: + _add_incremental_tools(cloned, split_csv(overrides.tools)) + + if "skills" in set_fields: + _add_incremental_skills(cloned, split_csv(overrides.skills), download_dir) + + return cloned From 6bcedfb30e3cbedf570df4cbbf3e471cede3cd6f Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 17:26:05 +0800 Subject: [PATCH 03/15] feat(harness): layered harness.yaml config + env_mapping; redesign create/add/deploy - harness.yaml (layered, per-component) is the agent spec; env_mapping.to_runtime_env reuses veadk flatten_dict for generic fields and maps each component's backend connection params to the veadk DATABASE_* env vars the backend reads - veadk harness create/add/deploy: create scaffolds harness.yaml + .env.example (AK/SK only) + domestic-source Dockerfile; add edits harness.yaml; deploy builds the image on AgentKit cloud and creates a runtime via sdk.launch - runtime env names aligned (MODEL_NAME / KNOWLEDGE_BASE_TYPE / LONG_TERM_MEMORY_TYPE / SHORT_TERM_MEMORY_TYPE) --- docs/content/docs/cli/harness-cli.en.mdx | 113 +++-- docs/content/docs/cli/harness-cli.mdx | 87 +++- veadk/cli/cli_agentkit.py | 2 +- veadk/cli/cli_harness.py | 564 ++++++++++++----------- veadk/cloud/harness_app/Dockerfile | 2 +- veadk/cloud/harness_app/agent.py | 20 +- veadk/cloud/harness_app/env_mapping.py | 165 +++++++ veadk/cloud/harness_app/utils.py | 10 +- 8 files changed, 623 insertions(+), 340 deletions(-) create mode 100644 veadk/cloud/harness_app/env_mapping.py diff --git a/docs/content/docs/cli/harness-cli.en.mdx b/docs/content/docs/cli/harness-cli.en.mdx index e1c9f4ff..f7127014 100644 --- a/docs/content/docs/cli/harness-cli.en.mdx +++ b/docs/content/docs/cli/harness-cli.en.mdx @@ -19,17 +19,19 @@ register and invoke harnesses at runtime without redeploying. ## Deploying the Harness server -The top-level `veadk harness` group scaffolds and deploys the Harness server -itself (this is separate from the `veadk agentkit harness` client below). +The top-level `veadk harness` group scaffolds, configures, and deploys the +Harness server itself (this is separate from the `veadk agentkit harness` client +below). The agent is configured through a layered `harness.yaml`. | Command | Description | | :-- | :-- | | `veadk harness create ` | Scaffold a deployable harness directory. | -| `veadk harness deploy` | Build and push the harness image, then create an AgentKit runtime. | +| `veadk harness add` | Write agent parameters into `harness.yaml`. | +| `veadk harness deploy` | Cloud-build the image and create an AgentKit runtime. | -### `harness create` +### `veadk harness create` -Create a deployment directory documenting every harness environment variable: +Create a deployment directory: ```bash veadk harness create my-harness @@ -37,49 +39,90 @@ veadk harness create my-harness This writes: -- `.env.example` — every harness env var (`MODEL_AGENT_NAME`, `MODEL_AGENT_API_KEY`, - `MODEL_AGENT_API_BASE`, `MODEL_AGENT_PROVIDER`, `SYSTEM_PROMPT`, `TOOLS`, `SKILLS`, - `RUNTIME`, `APP_NAME`, `KNOWLEDGEBASE_TYPE`, `LONGTERM_MEM_TYPE`, - `SHORTTERM_MEM_TYPE`, `HARNESS_NAME`, `SKILL_HUB_DOWNLOAD_URL`, `SERVER_HOST`, - `SERVER_PORT`) with comments and placeholders. -- `Dockerfile` — builds the image that serves the harness app. -- `agentkit.yaml` — AgentKit build config (hybrid: local build + push to - Container Registry); `ve_runtime_name` defaults to the harness name. +- `harness.yaml` — the agent configuration template (see below). +- `.env.example` — Volcengine deploy credentials only (`VOLCENGINE_ACCESS_KEY`, + `VOLCENGINE_SECRET_KEY`, optional `VOLCENGINE_REGION`). Model/agent config + lives in `harness.yaml`. +- `Dockerfile` — builds the harness server image. - `README.md` — quickstart. -### `harness deploy` +#### `harness.yaml` + +A layered configuration file. `deploy` flattens it into the runtime's +environment variables: nested keys are joined with `_` and upper-cased, lists +become comma-separated strings, and empty values are skipped. + +```yaml +harness_name: "" # -> HARNESS_NAME (runtime + KB/memory index name) +model: + name: "" # -> MODEL_NAME +tools: [] # -> TOOLS (comma-joined) +skills: [] # -> SKILLS (comma-joined) +system_prompt: "" # -> SYSTEM_PROMPT +runtime: adk # -> RUNTIME ("adk" or "codex") +knowledge_base: + type: "" # -> KNOWLEDGE_BASE_TYPE +long_term_memory: + type: "" # -> LONG_TERM_MEMORY_TYPE +short_term_memory: + type: local # -> SHORT_TERM_MEMORY_TYPE +``` + +On the AgentKit runtime, Ark auth is resolved from the runtime's IAM role, so no +model API key is configured here. Future parameters (extra memory/KB settings) +can be added under each section. -Copy `.env.example` to `.env`, fill it in (set `HARNESS_NAME`), set a real -`cr_instance_name` in `agentkit.yaml` (or run `agentkit config`), then from -inside the directory: +### `veadk harness add` + +Write agent parameters into `./harness.yaml` (or `--path`). Scalar options SET +their value; `--tool` / `--skill` are repeatable and APPEND to the lists +(deduped). Fast-fails when `harness.yaml` is missing. ```bash cd my-harness -cp .env.example .env # then edit it (set HARNESS_NAME) -# edit agentkit.yaml: set cr_instance_name (or run `agentkit config`) +veadk harness add \ + --harness-name research-agent \ + --model-name doubao-seed-1-6-250615 \ + --system-prompt "You are a research assistant." \ + --tool web_search --tool web_fetch \ + --runtime adk +``` + +| Option | Description | +| :-- | :-- | +| `--harness-name` | Logical harness / runtime name (sets `harness_name`). | +| `--model-name` | Reasoning model name (sets `model.name`). | +| `--tool` | Built-in tool name to append to `tools` (repeatable). | +| `--skill` | Skill hub name to append to `skills` (repeatable). | +| `--system-prompt` | System prompt / instruction. | +| `--runtime` | Agent runtime backend, `adk` or `codex`. | +| `--knowledge-base-type` | Knowledge base backend (sets `knowledge_base.type`). | +| `--long-term-memory-type` | Long-term memory backend (sets `long_term_memory.type`). | +| `--short-term-memory-type` | Short-term memory backend (sets `short_term_memory.type`). | +| `--path` | Path to `harness.yaml`, default `./harness.yaml`. | + +### `veadk harness deploy` + +Fill in deploy credentials, then from inside the directory: + +```bash +cd my-harness +cp .env.example .env # then set VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY veadk harness deploy ``` | Option | Required | Description | | :-- | :-- | :-- | -| `--volcengine-access-key` | No | Volcengine access key (defaults to `VOLCENGINE_ACCESS_KEY` / `VOLC_ACCESSKEY`). | -| `--volcengine-secret-key` | No | Volcengine secret key (defaults to `VOLCENGINE_SECRET_KEY` / `VOLC_SECRETKEY`). | +| `--volcengine-access-key` | No | Volcengine access key (defaults to `VOLCENGINE_ACCESS_KEY`). | +| `--volcengine-secret-key` | No | Volcengine secret key (defaults to `VOLCENGINE_SECRET_KEY`). | | `--region` | No | AgentKit region (default `cn-beijing` or `VOLCENGINE_REGION`). | -| `--role-name` | No | Runtime IAM role name (default: auto-generated or `HARNESS_RUNTIME_ROLE`). | | `--path` | No | Harness directory, default `.`. | -`deploy` builds and pushes the harness image via AgentKit's hybrid build (local -`docker build` + push to Container Registry), then calls AgentKit's -`CreateRuntime` to create a runtime whose: - -- **Name** is `HARNESS_NAME` from your `.env` (default `default` when unset), -- **Tags** carry a single tag whose key is the literal `Harness` (key only, no value), -- **Envs** are the directory's `.env`, -- **ArtifactType** is `image` and **ArtifactUrl** is the freshly pushed image. - -On success it prints the runtime's name and id; look up its endpoint in the -AgentKit console or via the AgentKit SDK (`GetRuntime`), then pass it to -`veadk agentkit harness invoke --url ...`. +`deploy` loads `harness.yaml`, flattens it into the runtime's environment, and +runs an AgentKit **cloud** build (no local Docker) plus runtime create. The +runtime is named after `harness_name` (default `default`). On success it prints +the runtime name and its service endpoint; you can also find the endpoint in the +AgentKit console, then pass it to `veadk agentkit harness invoke --url ...`. ## Client command reference @@ -118,7 +161,7 @@ veadk agentkit harness add \ | Option | Required | Description | | :-- | :-- | :-- | | `--name` | Yes | Harness (agent) name. | -| `--model-name` | No | Model name; defaults to the server's `MODEL_AGENT_NAME`. | +| `--model-name` | No | Model name; defaults to the server's `MODEL_NAME`. | | `--system-prompt` | No | System prompt; defaults to `You are a helpful assistant.`. | | `--tools` | No | Comma-separated built-in tool names, e.g. `web_search,web_fetch`. | | `--skills` | No | Comma-separated skill hub names, e.g. `clawhub/lgwventrue/system-file-handler`. | diff --git a/docs/content/docs/cli/harness-cli.mdx b/docs/content/docs/cli/harness-cli.mdx index 17a8d801..8299bf66 100644 --- a/docs/content/docs/cli/harness-cli.mdx +++ b/docs/content/docs/cli/harness-cli.mdx @@ -18,11 +18,14 @@ description: "通过命令行注册并调用部署在 Harness server 上的 harn | 命令 | 描述 | | :-- | :-- | | `veadk harness create ` | 生成一个可部署的 harness 目录。 | -| `veadk harness deploy` | 构建并推送 harness 镜像,然后创建一个 AgentKit runtime。 | +| `veadk harness add` | 将智能体参数写入 `harness.yaml`。 | +| `veadk harness deploy` | 云端构建镜像并创建一个 AgentKit runtime。 | -### `harness create` +智能体通过分层的 `harness.yaml` 进行配置。 -生成一个部署目录,并在其中以注释形式说明所有 harness 环境变量: +### `veadk harness create` + +生成一个部署目录: ```bash veadk harness create my-harness @@ -30,38 +33,78 @@ veadk harness create my-harness 该命令会写入: -- `.env.example` —— 列出全部 harness 环境变量(`MODEL_AGENT_NAME`、`MODEL_AGENT_API_KEY`、`MODEL_AGENT_API_BASE`、`MODEL_AGENT_PROVIDER`、`SYSTEM_PROMPT`、`TOOLS`、`SKILLS`、`RUNTIME`、`APP_NAME`、`KNOWLEDGEBASE_TYPE`、`LONGTERM_MEM_TYPE`、`SHORTTERM_MEM_TYPE`、`HARNESS_NAME`、`SKILL_HUB_DOWNLOAD_URL`、`SERVER_HOST`、`SERVER_PORT`),并附带注释与占位值。 -- `Dockerfile` —— 构建提供 harness 应用服务的镜像。 -- `agentkit.yaml` —— AgentKit 构建配置(hybrid:本地构建 + 推送到容器镜像仓库 CR),其中 `ve_runtime_name` 默认为 harness 名称。 +- `harness.yaml` —— 智能体配置模板(见下)。 +- `.env.example` —— 仅包含火山引擎部署凭证(`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`,以及可选的 `VOLCENGINE_REGION`)。模型/智能体配置位于 `harness.yaml`。 +- `Dockerfile` —— 构建 harness 服务镜像。 - `README.md` —— 快速开始说明。 -### `harness deploy` +#### `harness.yaml` + +分层配置文件。`deploy` 会将其展平为 runtime 的环境变量:嵌套键以 `_` 连接并转为大写,列表转为逗号分隔字符串,空值会被跳过。 + +```yaml +harness_name: "" # -> HARNESS_NAME(runtime 名 + 知识库/记忆索引名) +model: + name: "" # -> MODEL_NAME +tools: [] # -> TOOLS(逗号连接) +skills: [] # -> SKILLS(逗号连接) +system_prompt: "" # -> SYSTEM_PROMPT +runtime: adk # -> RUNTIME("adk" 或 "codex") +knowledge_base: + type: "" # -> KNOWLEDGE_BASE_TYPE +long_term_memory: + type: "" # -> LONG_TERM_MEMORY_TYPE +short_term_memory: + type: local # -> SHORT_TERM_MEMORY_TYPE +``` + +在 AgentKit runtime 上,Ark 鉴权由 runtime 的 IAM 角色解析,因此此处无需配置模型 API Key。未来的参数(额外的记忆/知识库设置)可以在各分段下扩展。 + +### `veadk harness add` + +将智能体参数写入 `./harness.yaml`(或 `--path`)。标量选项为「设置」语义;`--tool` / `--skill` 可重复,向列表「追加」(去重)。当 `harness.yaml` 不存在时快速失败。 + +```bash +cd my-harness +veadk harness add \ + --harness-name research-agent \ + --model-name doubao-seed-1-6-250615 \ + --system-prompt "You are a research assistant." \ + --tool web_search --tool web_fetch \ + --runtime adk +``` + +| 选项 | 说明 | +| :-- | :-- | +| `--harness-name` | 逻辑 harness / runtime 名(设置 `harness_name`)。 | +| `--model-name` | 推理模型名(设置 `model.name`)。 | +| `--tool` | 追加到 `tools` 的内置工具名(可重复)。 | +| `--skill` | 追加到 `skills` 的技能名(可重复)。 | +| `--system-prompt` | 系统提示词 / 指令。 | +| `--runtime` | 智能体 runtime,`adk` 或 `codex`。 | +| `--knowledge-base-type` | 知识库后端(设置 `knowledge_base.type`)。 | +| `--long-term-memory-type` | 长期记忆后端(设置 `long_term_memory.type`)。 | +| `--short-term-memory-type` | 短期记忆后端(设置 `short_term_memory.type`)。 | +| `--path` | `harness.yaml` 路径,默认 `./harness.yaml`。 | + +### `veadk harness deploy` -将 `.env.example` 复制为 `.env` 并填好(务必设置 `HARNESS_NAME`),并在 `agentkit.yaml` 中设置一个真实的 `cr_instance_name`(或运行 `agentkit config`),然后在该目录内执行: +填好部署凭证后,在该目录内执行: ```bash cd my-harness -cp .env.example .env # 然后编辑它(设置 HARNESS_NAME) -# 编辑 agentkit.yaml:设置 cr_instance_name(或运行 `agentkit config`) +cp .env.example .env # 然后设置 VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY veadk harness deploy ``` | 选项 | 必填 | 说明 | | :-- | :-- | :-- | -| `--volcengine-access-key` | 否 | 火山引擎 Access Key(默认读取 `VOLCENGINE_ACCESS_KEY` / `VOLC_ACCESSKEY`)。 | -| `--volcengine-secret-key` | 否 | 火山引擎 Secret Key(默认读取 `VOLCENGINE_SECRET_KEY` / `VOLC_SECRETKEY`)。 | +| `--volcengine-access-key` | 否 | 火山引擎 Access Key(默认读取 `VOLCENGINE_ACCESS_KEY`)。 | +| `--volcengine-secret-key` | 否 | 火山引擎 Secret Key(默认读取 `VOLCENGINE_SECRET_KEY`)。 | | `--region` | 否 | AgentKit 区域(默认 `cn-beijing` 或 `VOLCENGINE_REGION`)。 | -| `--role-name` | 否 | runtime IAM 角色名(默认自动生成,或读取 `HARNESS_RUNTIME_ROLE`)。 | | `--path` | 否 | harness 目录,默认 `.`。 | -`deploy` 会通过 AgentKit 的 hybrid 构建(本地 `docker build` + 推送到容器镜像仓库 CR)构建并推送 harness 镜像,然后调用 AgentKit 的 `CreateRuntime` 创建一个 runtime: - -- **Name** 取自 `.env` 中的 `HARNESS_NAME`(未设置时默认 `default`); -- **Tags** 携带一个内容为字面量 `Harness` 的标签(仅 Key,无 Value); -- **Envs** 取自该目录的 `.env`; -- **ArtifactType** 为 `image`,**ArtifactUrl** 为刚推送的镜像地址。 - -成功后会打印 runtime 的 name 与 id;可在 AgentKit 控制台或通过 AgentKit SDK(`GetRuntime`)查询其 Endpoint,再传给 `veadk agentkit harness invoke --url ...`。 +`deploy` 会加载 `harness.yaml`,将其展平为 runtime 的环境变量,并执行 AgentKit 的**云端**构建(无需本地 Docker)以及 runtime 创建。runtime 以 `harness_name` 命名(默认 `default`)。成功后会打印 runtime 名及其服务 Endpoint;也可在 AgentKit 控制台查询 Endpoint,再传给 `veadk agentkit harness invoke --url ...`。 ## 客户端命令一览 @@ -98,7 +141,7 @@ veadk agentkit harness add \ | 选项 | 必填 | 说明 | | :-- | :-- | :-- | | `--name` | 是 | harness(智能体)名称。 | -| `--model-name` | 否 | 模型名称,缺省使用服务端的 `MODEL_AGENT_NAME`。 | +| `--model-name` | 否 | 模型名称,缺省使用服务端的 `MODEL_NAME`。 | | `--system-prompt` | 否 | 系统提示词,默认 `You are a helpful assistant.`。 | | `--tools` | 否 | 逗号分隔的内置工具名,如 `web_search,web_fetch`。 | | `--skills` | 否 | 逗号分隔的技能 hub 名,如 `clawhub/lgwventrue/system-file-handler`。 | diff --git a/veadk/cli/cli_agentkit.py b/veadk/cli/cli_agentkit.py index 0b136503..5b0229ae 100644 --- a/veadk/cli/cli_agentkit.py +++ b/veadk/cli/cli_agentkit.py @@ -73,7 +73,7 @@ def harness() -> None: "--model-name", "model_name", default=None, - help="Model name for the harness (defaults to the server's MODEL_AGENT_NAME).", + help="Model name for the harness (defaults to the server's MODEL_NAME).", ) @click.option( "--system-prompt", diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index 469e09de..5675a946 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -14,14 +14,15 @@ """Top-level ``veadk harness`` command group. -Two subcommands scaffold and deploy the harness server -(:mod:`veadk.cloud.harness_app`): +Subcommands scaffold, configure, and deploy the harness server +(:mod:`veadk.cloud.harness_app`) from a layered ``harness.yaml``: -* ``veadk harness create `` writes a deployable directory documenting every - harness environment variable in ``.env.example`` (plus a ``Dockerfile`` and an - ``agentkit.yaml`` preconfigured for an AgentKit runtime deploy). -* ``veadk harness deploy`` builds and pushes the harness Docker image via - AgentKit, then creates an **AgentKit runtime** from that image. +* ``veadk harness create `` writes a deployable directory: a blank + ``harness.yaml`` template, a ``.env.example`` (deploy credentials only), a + ``Dockerfile``, and a short ``README.md``. +* ``veadk harness add`` writes agent parameters into ``harness.yaml``. +* ``veadk harness deploy`` flattens ``harness.yaml`` into runtime env vars and + performs a cloud AgentKit build + runtime create (no local Docker). This group is independent of ``veadk agentkit harness``, which is an HTTP client for an *already deployed* server. @@ -32,176 +33,155 @@ from pathlib import Path import click +import yaml +from veadk.cloud.harness_app.env_mapping import to_runtime_env from veadk.cloud.harness_app.types import HarnessOverrides -# AgentKit runtime artifact type for a container image (defined in -# `agentkit/toolkit/runners/ve_agentkit.py::ARTIFACT_TYPE_DOCKER_IMAGE`). -_ARTIFACT_TYPE_IMAGE = "image" - -# Tag attached to every harness runtime so it can be discovered later. The -# locked requirement is a single tag whose key is the literal "Harness" with no -# value. -_HARNESS_TAG_KEY = "Harness" - -# Default harness/runtime name when `HARNESS_NAME` is unset in the `.env` +# Default harness/runtime name when `harness_name` is unset in `harness.yaml` # (mirrors `veadk.cloud.harness_app.app.HARNESS_NAME`). _DEFAULT_HARNESS_NAME = "default" -# Documents every harness env var read by `veadk.cloud.harness_app.agent` -# (authoritative source: `harness_app/utils.py::_ENV_FIELDS` and the -# `agent.py` module docstring). Placeholder values are safe defaults; the -# server falls back to VeADK defaults for anything left unset. +# Blank `harness.yaml` template written by `create`. Layered sections map to the +# flattened runtime env names consumed by `veadk.cloud.harness_app` (see +# `flatten`): `model.name` -> MODEL_NAME, `knowledge_base.type` -> +# KNOWLEDGE_BASE_TYPE, etc. Empty values are skipped on flatten. +_HARNESS_YAML = """\ +# VeADK harness configuration. `veadk harness deploy` flattens this file into the +# runtime's environment variables (nested keys joined with `_` and upper-cased; +# lists become comma-separated; empty values are skipped). + +# Logical harness name; also the runtime name and the knowledge-base / long-term +# memory index name. Defaults to "default" when empty. +harness_name: "" + +# Reasoning model. On the AgentKit runtime, Ark auth is resolved from the +# runtime's IAM role, so only the model name is needed here. Extra model +# settings can be added under this section in the future. +model: + name: "" + +# Comma-flattened built-in tool names, e.g. [web_search, web_fetch]. +tools: [] + +# Skill hub names, e.g. [clawhub/lgwventrue/system-file-handler]. +skills: [] + +# System prompt / instruction. Empty uses the VeADK default instruction. +system_prompt: "" + +# Agent runtime backend: "adk" (default) or "codex". "codex" requires the +# optional codex extra installed on the server image. +runtime: adk + +# Knowledge base backend (e.g. "viking"). Empty disables it. Extra backend +# settings can be added under this section in the future. +knowledge_base: + type: "" + +# Long-term memory backend (e.g. "viking"). Empty disables it. +long_term_memory: + type: "" + +# Short-term memory backend (e.g. "local", "mysql"). Defaults to "local". +short_term_memory: + type: local +""" + +# `.env.example` carries ONLY deploy credentials. All model / agent config lives +# in `harness.yaml`; on the runtime, Ark auth is resolved from the IAM role. _ENV_EXAMPLE = """\ -# --------------------------------------------------------------------------- -# Harness server environment variables. -# Copy to `.env` and fill in. Only set what you need; unset vars fall back to -# the VeADK defaults documented below. -# --------------------------------------------------------------------------- - -# --- Reasoning model ------------------------------------------------------- -# Model name. Unset uses the VeADK default model. -MODEL_AGENT_NAME=doubao-seed-1-6-250615 -# Model API credentials / endpoint (Volcengine Ark by default). -MODEL_AGENT_API_KEY=your-ark-api-key -MODEL_AGENT_API_BASE=https://ark.cn-beijing.volces.com/api/v3 -MODEL_AGENT_PROVIDER=openai - -# --- Agent definition ------------------------------------------------------ -# System prompt / instruction. Unset uses the VeADK default instruction. -SYSTEM_PROMPT=You are a helpful assistant. -# Comma-separated built-in tool names, e.g. "web_search,web_fetch". -TOOLS= -# Comma-separated skill hub names, e.g. "clawhub/lgwventrue/system-file-handler". -SKILLS= -# Agent runtime backend: "adk" (default) or "codex". -# "codex" requires the optional codex extra installed on the server. -RUNTIME=adk - -# --- Knowledge base & memory ---------------------------------------------- -# App/index name for the knowledge base and long-term memory. Default: harness_app. -APP_NAME=harness_app -# Knowledge base backend (e.g. "viking"). Leave empty to disable. -KNOWLEDGEBASE_TYPE= -# Long-term memory backend (e.g. "viking"). Leave empty to disable. -LONGTERM_MEM_TYPE= -# Short-term memory backend (e.g. "local", "mysql"). Default: local. -SHORTTERM_MEM_TYPE=local - -# --- Server ---------------------------------------------------------------- -# Logical harness name reported in invoke responses. Default: default. -HARNESS_NAME=default -# Skill hub download endpoint. Unset uses the public skill hub. -SKILL_HUB_DOWNLOAD_URL=https://skills.volces.com/v1/skills/download -# Bind host/port. On VeFaaS these are set automatically from the runtime port. -SERVER_HOST=0.0.0.0 -SERVER_PORT=8000 +# Volcengine deploy credentials for `veadk harness deploy`. Copy to `.env` and +# fill in. These authenticate the AgentKit cloud build + runtime create. +VOLCENGINE_ACCESS_KEY= +VOLCENGINE_SECRET_KEY= +# VOLCENGINE_REGION=cn-beijing """ -# Container image entrypoint, mirroring `veadk/cloud/harness_app/Dockerfile` -# but importing the app from the installed package (no source files copied). +# Container image for the harness server. The base image's apt mirror is an +# unreachable internal host, so apt is repointed at aliyun; the source branch is +# cloned via the ghfast proxy with a github fallback; uv installs from aliyun. _DOCKERFILE = """\ -FROM python:3.12-slim - +FROM agentkit-cn-beijing.cr.volces.com/base/py-simple:python3.12-bookworm-slim-latest +ENV PYTHONUNBUFFERED=1 +RUN set -eux; \\ + rm -f /etc/apt/sources.list.d/*; \\ + printf 'deb http://mirrors.aliyun.com/debian bookworm main contrib non-free non-free-firmware\\n\\ +deb http://mirrors.aliyun.com/debian bookworm-updates main contrib non-free non-free-firmware\\n\\ +deb http://mirrors.aliyun.com/debian-security bookworm-security main contrib non-free non-free-firmware\\n' \\ + > /etc/apt/sources.list; \\ + apt-get update; \\ + apt-get install -y --no-install-recommends git ca-certificates; \\ + rm -rf /var/lib/apt/lists/* WORKDIR /app - -# `[extensions]` pulls llama-index / redis / opensearch, required when the -# KNOWLEDGEBASE_TYPE or LONGTERM_MEM_TYPE env vars enable those components. -RUN apt-get update && apt-get install -y --no-install-recommends git && \\ - pip3 install --no-cache-dir \\ - "veadk-python[extensions] @ git+https://github.com/volcengine/veadk-python.git" && \\ - apt-get purge -y git && apt-get autoremove -y && \\ - apt-get clean && rm -rf /var/lib/apt/lists/* - -# To run with the "codex" runtime (RUNTIME=codex), also install: -# pip3 install --no-cache-dir openai-codex - +RUN set -eux; \\ + for url in \\ + https://ghfast.top/https://github.com/volcengine/veadk-python.git \\ + https://github.com/volcengine/veadk-python.git ; do \\ + for i in 1 2 3; do \\ + git clone --depth 1 -b feat/harness-runtime "$url" src && break 2 || sleep 8; \\ + done; \\ + done; \\ + test -d src/veadk +RUN uv pip install --system --index-url https://mirrors.aliyun.com/pypi/simple/ \\ + ./src fastapi "uvicorn[standard]" EXPOSE 8000 - -CMD ["python", "-m", "uvicorn", "veadk.cloud.harness_app.app:app", \\ - "--host", "0.0.0.0", "--port", "8000"] -""" - -# AgentKit deploy config consumed by `agentkit.toolkit.sdk.build`. `launch_type: -# hybrid` builds the image locally with Docker and pushes it to Container -# Registry, which yields the pushed image URL `veadk harness deploy` feeds to -# CreateRuntime. The hand-written `Dockerfile` above has no AgentKit metadata -# header, so AgentKit keeps it as-is (KEEP_USER_CUSTOM) instead of regenerating. -# `ve_runtime_name` defaults to the harness name; `veadk harness deploy` -# overrides it from `HARNESS_NAME` in the `.env` anyway. -_AGENTKIT_YAML = """\ -# AgentKit build config for `veadk harness deploy`. `deploy` runs the hybrid -# build (local Docker build + push to Container Registry) and then creates an -# AgentKit runtime from the pushed image. Set a real `cr_instance_name` (or run -# `agentkit config`) so the image can be pushed; `Auto` skips the push. -common: - agent_name: harness - entry_point: app.py - description: VeADK harness server - language: Python - language_version: "3.12" - launch_type: hybrid -launch_types: - hybrid: - region: cn-beijing - ve_runtime_name: {runtime_name} - cr_instance_name: Auto - cr_namespace_name: agentkit - cr_repo_name: harness +CMD ["python", "-m", "uvicorn", "veadk.cloud.harness_app.app:app", "--host", "0.0.0.0", "--port", "8000"] """ _README = """\ # Harness deployment directory -This directory deploys the VeADK harness server -(`veadk.cloud.harness_app.app:app`) as a Volcengine **AgentKit runtime**. +Deploys the VeADK harness server (`veadk.cloud.harness_app.app:app`) as a +Volcengine **AgentKit runtime** (cloud build, no local Docker). ## Files -- `.env.example` — every harness env var; copy to `.env` and fill in. -- `Dockerfile` — builds the image that serves the harness app. -- `agentkit.yaml` — AgentKit build config (hybrid: local build + push to - Container Registry). `ve_runtime_name` defaults to the harness name. +- `harness.yaml` — agent configuration; flattened into runtime env vars. +- `.env.example` — Volcengine deploy credentials; copy to `.env` and fill in. +- `Dockerfile` — builds the harness server image. - `README.md` — this file. ## Usage -1. Copy `.env.example` to `.env` and fill in the values (model key, tools, - skills…). Set `HARNESS_NAME`; it becomes the runtime's name (defaults to - `default` when unset). -2. Set a real Container Registry instance in `agentkit.yaml` (`cr_instance_name`) - or run `agentkit config` — `Auto` skips the image push and the build cannot - produce an image URL. -3. From inside this directory, run: +1. Configure the agent: ```bash - veadk harness deploy + veadk harness add --harness-name my-harness --model-name doubao-seed-1-6-250615 \\ + --tool web_search --system-prompt "You are a helpful assistant." + ``` + +2. Fill in deploy credentials: + + ```bash + cp .env.example .env # then set VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY ``` -`deploy` builds and pushes the Docker image via AgentKit, then creates an -AgentKit runtime named after `HARNESS_NAME` with a `Harness` tag. The env vars -in `.env` become the runtime's environment, so the cloud agent is assembled -exactly as configured here. +3. Deploy (cloud build + runtime create): + + ```bash + veadk harness deploy + ``` """ _CREATE_SUCCESS = """\ Harness deployment directory created at {target}: -- .env.example (copy to .env and fill in) +- harness.yaml (agent configuration) +- .env.example (copy to .env and set VOLCENGINE_ACCESS_KEY / SECRET_KEY) - Dockerfile (builds the harness image) -- agentkit.yaml (AgentKit build config; set a real cr_instance_name) - README.md Next steps: cd {name} - cp .env.example .env # then edit it (set HARNESS_NAME) - # edit agentkit.yaml: set cr_instance_name (or run `agentkit config`) + veadk harness add --harness-name my-harness --model-name + cp .env.example .env # then set VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY veadk harness deploy """ @click.group() def harness() -> None: - """Create and deploy a VeADK harness server.""" + """Create, configure, and deploy a VeADK harness server.""" pass @@ -210,10 +190,9 @@ def harness() -> None: def create(dir_name: str) -> None: """Scaffold a deployable harness directory at DIR_NAME. - Writes a `.env.example` documenting every harness environment variable, a - `Dockerfile` that builds the harness image, an `agentkit.yaml` build config, - and a short README. Copy `.env.example` to `.env`, fill it in, then run - `veadk harness deploy` from inside the directory. + Writes a blank `harness.yaml` template, a `.env.example` (deploy credentials + only), a `Dockerfile`, and a short README. Configure the agent with + `veadk harness add`, fill in `.env`, then run `veadk harness deploy`. """ target = Path.cwd() / dir_name if target.exists() and any(target.iterdir()): @@ -223,106 +202,165 @@ def create(dir_name: str) -> None: ) target.mkdir(parents=True, exist_ok=True) + (target / "harness.yaml").write_text(_HARNESS_YAML) (target / ".env.example").write_text(_ENV_EXAMPLE) (target / "Dockerfile").write_text(_DOCKERFILE) - (target / "agentkit.yaml").write_text( - _AGENTKIT_YAML.format(runtime_name=_DEFAULT_HARNESS_NAME) - ) (target / "README.md").write_text(_README) click.secho(_CREATE_SUCCESS.format(target=target, name=dir_name), fg="green") -def _read_env_file(env_path: Path) -> dict[str, str]: - """Parse the harness directory's `.env` into a flat ``{KEY: VALUE}`` dict. +def _load_harness_yaml(path: Path) -> dict: + """Load ``harness.yaml`` into a dict; fast-fail when it is missing.""" + if not path.is_file(): + raise click.ClickException( + f"No `harness.yaml` at '{path}'. Run `veadk harness create` first." + ) + return yaml.safe_load(path.read_text()) or {} - Returns an empty dict when no `.env` exists. Used both to derive the runtime - name (`HARNESS_NAME`) and as the runtime's `Envs`. - """ - from dotenv import dotenv_values - if not env_path.is_file(): - return {} - return {k: v for k, v in dotenv_values(env_path).items() if v is not None} +def _append_dedup(data: dict, key: str, values: tuple[str, ...]) -> None: + """Append ``values`` to the list at ``data[key]``, preserving order, deduped.""" + existing = data.get(key) or [] + if not isinstance(existing, list): + existing = [existing] + for value in values: + if value not in existing: + existing.append(value) + data[key] = existing -def _build_harness_image(proj_dir: Path) -> str: - """Build and push the harness image via AgentKit; return the pushed image URL. +@harness.command("add") +@click.option("--harness-name", default=None, help="Logical harness / runtime name.") +@click.option("--model-name", default=None, help="Reasoning model name.") +@click.option( + "--tool", + "tools", + multiple=True, + help="Built-in tool name to append to `tools` (repeatable).", +) +@click.option( + "--skill", + "skills", + multiple=True, + help="Skill hub name to append to `skills` (repeatable).", +) +@click.option("--system-prompt", default=None, help="System prompt / instruction.") +@click.option( + "--runtime", + type=click.Choice(["adk", "codex"]), + default=None, + help="Agent runtime backend.", +) +@click.option("--knowledge-base-type", default=None, help="Knowledge base backend.") +@click.option("--long-term-memory-type", default=None, help="Long-term memory backend.") +@click.option( + "--short-term-memory-type", default=None, help="Short-term memory backend." +) +@click.option( + "--path", + default=".", + help="Harness directory containing harness.yaml (default: current dir).", +) +def add( + harness_name: str | None, + model_name: str | None, + tools: tuple[str, ...], + skills: tuple[str, ...], + system_prompt: str | None, + runtime: str | None, + knowledge_base_type: str | None, + long_term_memory_type: str | None, + short_term_memory_type: str | None, + path: str, +) -> None: + """Write agent parameters into `harness.yaml`. - Reuses AgentKit's `sdk.build` (hybrid strategy: local Docker build + push to - Container Registry). The pushed image URL is read from the build result's - `metadata["cr_image_url"]` (the push step records it there), falling back to - the local image's full name. Fast-fails when the build did not push an image. + Scalar options SET their value; `--tool` / `--skill` are repeatable and + APPEND to the existing lists (deduped). Operates on `./harness.yaml` unless + `--path` is given; fast-fails when the file is missing. """ - from agentkit.toolkit import sdk - from agentkit.toolkit.reporter import LoggingReporter - - config_file = proj_dir / "agentkit.yaml" - if not config_file.is_file(): - raise click.ClickException( - f"No `agentkit.yaml` in '{proj_dir}'. Run `veadk harness create` first." - ) - - result = sdk.build(config_file=str(config_file), reporter=LoggingReporter()) - if not result.success: - raise click.ClickException(f"Harness image build failed: {result.error}") - - image_url = (result.metadata or {}).get("cr_image_url") - if not image_url: - raise click.ClickException( - "Build succeeded but no image was pushed to Container Registry. Set a " - "real `cr_instance_name` in `agentkit.yaml` (or run `agentkit config`)." - ) - return image_url - - -def _create_harness_runtime( - *, - runtime_name: str, - role_name: str, - image_url: str, - envs: dict[str, str], - region: str, -) -> str: - """Create an AgentKit runtime for the harness image; return its runtime id. - - Ensures the IAM role exists (CreateRuntime requires it), then issues - CreateRuntime with the harness name, a `Harness` tag, the `.env` as `Envs`, - and the pushed image as an `image` artifact. + yaml_path = Path(path).resolve() / "harness.yaml" + data = _load_harness_yaml(yaml_path) + + if harness_name is not None: + data["harness_name"] = harness_name + if model_name is not None: + model = data.get("model") + if not isinstance(model, dict): + model = {} + model["name"] = model_name + data["model"] = model + if system_prompt is not None: + data["system_prompt"] = system_prompt + if runtime is not None: + data["runtime"] = runtime + # Set only the backend `type`, preserving any connection params already set + # under the component section. + for type_value, section_key in ( + (knowledge_base_type, "knowledge_base"), + (long_term_memory_type, "long_term_memory"), + (short_term_memory_type, "short_term_memory"), + ): + if type_value is not None: + section = data.get(section_key) + if not isinstance(section, dict): + section = {} + section["type"] = type_value + data[section_key] = section + + if tools: + _append_dedup(data, "tools", tools) + if skills: + _append_dedup(data, "skills", skills) + + yaml_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True)) + click.secho(f"Updated {yaml_path}", fg="green") + + +def _build_agentkit_config( + runtime_name: str, region: str, envs: dict[str, str] +) -> dict: + """Build the cloud AgentKit launch config dict (auto-provision). + + Mirrors the structure `agentkit init` produces for `launch_type: cloud`. The + `{{account_id}}` / `{{timestamp}}` templates are resolved by AgentKit at + deploy time and are passed through literally. """ - from agentkit.sdk.runtime import types as rt - from agentkit.sdk.runtime.client import AgentkitRuntimeClient - from agentkit.toolkit.volcengine.iam import VeIAM - from agentkit.utils.misc import generate_apikey_name, generate_client_token - - if not VeIAM(region=region).ensure_role_for_agentkit(role_name): - raise click.ClickException( - f"Failed to create or ensure the runtime IAM role `{role_name}`." - ) - - client = AgentkitRuntimeClient(region=region) - # The runtime types use by-alias (PascalCase) fields with `populate_by_name`; - # build the request from an alias-keyed dict via `model_validate` so static - # type checking matches the API field names exactly. - request = rt.CreateRuntimeRequest.model_validate( - { - "Name": runtime_name, - "RoleName": role_name, - "ArtifactType": _ARTIFACT_TYPE_IMAGE, - "ArtifactUrl": image_url, - "Envs": [{"Key": k, "Value": v} for k, v in envs.items()], - "Tags": [{"Key": _HARNESS_TAG_KEY}], - "AuthorizerConfiguration": { - "KeyAuth": { - "ApiKeyName": generate_apikey_name(), - "ApiKeyLocation": "HEADER", - } - }, - "ClientToken": generate_client_token(), - } - ) - response = client.create_runtime(request) - return response.runtime_id or "" + return { + "common": { + "agent_name": runtime_name, + "entry_point": "app.py", + "description": "VeADK harness server", + "language": "Python", + "language_version": "3.12", + "runtime_envs": envs, + "launch_type": "cloud", + }, + "launch_types": { + "cloud": { + "region": region, + "tos_bucket": "agentkit-platform-{{account_id}}", + "tos_prefix": "agentkit-builds", + "image_tag": "{{timestamp}}", + "cr_instance_name": "agentkit-platform-{{account_id}}", + "cr_namespace_name": "agentkit", + "cr_repo_name": runtime_name, + "cr_auto_create_instance_type": "Micro", + "build_timeout": 3600, + "cp_workspace_name": "agentkit-cli-workspace", + "cp_pipeline_name": "Auto", + "runtime_id": "Auto", + "runtime_name": runtime_name, + "runtime_role_name": "Auto", + "runtime_auth_type": "key_auth", + "runtime_apikey_name": "Auto", + "runtime_apikey": "Auto", + "runtime_jwt_allowed_clients": [], + } + }, + "docker_build": {}, + } @harness.command("deploy") @@ -333,11 +371,6 @@ def _create_harness_runtime( default=None, help="AgentKit region (default `cn-beijing` or VOLCENGINE_REGION).", ) -@click.option( - "--role-name", - default=None, - help="Runtime IAM role name (default: auto-generated or HARNESS_RUNTIME_ROLE).", -) @click.option( "--path", default=".", @@ -347,22 +380,20 @@ def deploy( volcengine_access_key: str | None, volcengine_secret_key: str | None, region: str | None, - role_name: str | None, path: str, ) -> None: - """Build the harness image and deploy it as an AgentKit runtime. - - Run this from inside a directory created by `veadk harness create` (with a - filled-in `.env` and an `agentkit.yaml`). It builds and pushes the harness - Docker image via AgentKit, then creates an AgentKit runtime whose: + """Deploy the harness as an AgentKit runtime (cloud build, no local Docker). - * Name is the harness name (`HARNESS_NAME` in the `.env`, default `default`), - * Envs are the directory's `.env`, - * Tags carry a single `Harness` tag, - * artifact is the pushed image. + Loads `harness.yaml`, flattens it into the runtime's environment, and runs an + AgentKit cloud build + runtime create. Run from inside a directory created by + `veadk harness create` (containing `harness.yaml` and the `Dockerfile`). """ import os + from agentkit.toolkit import sdk + from agentkit.toolkit.models import PreflightMode + from agentkit.toolkit.reporter import LoggingReporter + from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -371,9 +402,13 @@ def deploy( if not proj_dir.is_dir(): raise click.ClickException(f"Path '{proj_dir}' is not a directory.") - # AgentKit's build/runtime clients authenticate via the Volcengine SDK, which - # reads VOLC_ACCESSKEY / VOLC_SECRETKEY from the environment. Mirror whatever - # AK/SK was passed (or already set as VOLCENGINE_*) into those names. + data = _load_harness_yaml(proj_dir / "harness.yaml") + runtime_envs = to_runtime_env(data) + runtime_name = data.get("harness_name") or _DEFAULT_HARNESS_NAME + + # AgentKit authenticates via the Volcengine SDK, which reads VOLC_ACCESSKEY / + # VOLC_SECRETKEY from the environment. Mirror whatever AK/SK was passed (or + # already set as VOLCENGINE_*) into those names. access_key = volcengine_access_key or os.getenv("VOLCENGINE_ACCESS_KEY", "") secret_key = volcengine_secret_key or os.getenv("VOLCENGINE_SECRET_KEY", "") if access_key and secret_key: @@ -383,35 +418,32 @@ def deploy( raise click.ClickException( "Volcengine credentials are required. Pass --volcengine-access-key / " "--volcengine-secret-key, or set VOLCENGINE_ACCESS_KEY / " - "VOLCENGINE_SECRET_KEY (or VOLC_ACCESSKEY / VOLC_SECRETKEY)." + "VOLCENGINE_SECRET_KEY." ) - envs = _read_env_file(proj_dir / ".env") - runtime_name = envs.get("HARNESS_NAME") or _DEFAULT_HARNESS_NAME resolved_region = region or os.getenv("VOLCENGINE_REGION") or "cn-beijing" - resolved_role = role_name or os.getenv("HARNESS_RUNTIME_ROLE") - if not resolved_role: - from agentkit.utils.misc import generate_runtime_role_name - - resolved_role = generate_runtime_role_name() - - logger.info(f"Building harness image from {proj_dir}") - image_url = _build_harness_image(proj_dir) - logger.info(f"Built harness image: {image_url}") - - runtime_id = _create_harness_runtime( - runtime_name=runtime_name, - role_name=resolved_role, - image_url=image_url, - envs=envs, - region=resolved_region, - ) + cfg = _build_agentkit_config(runtime_name, resolved_region, runtime_envs) + + logger.info(f"Deploying harness runtime '{runtime_name}' from {proj_dir}") + cwd = os.getcwd() + os.chdir(proj_dir) + try: + result = sdk.launch( + config_dict=cfg, + preflight_mode=PreflightMode.WARN, + reporter=LoggingReporter(), + ) + finally: + os.chdir(cwd) + + if not result.success: + raise click.ClickException(f"Harness deploy failed: {result.error}") + deploy_result = result.deploy_result + endpoint = deploy_result.endpoint_url if deploy_result else None click.secho( - f"Harness runtime created: name={runtime_name} id={runtime_id}\n" - f"Image: {image_url}\n" - f"Tagged `{_HARNESS_TAG_KEY}`. Find its endpoint in the AgentKit console " - "or via the AgentKit SDK (GetRuntime).", + f"Harness runtime deployed: name={runtime_name}\n" + f"Endpoint: {endpoint or '(see AgentKit console)'}", fg="green", ) diff --git a/veadk/cloud/harness_app/Dockerfile b/veadk/cloud/harness_app/Dockerfile index 07445d79..f1494eed 100644 --- a/veadk/cloud/harness_app/Dockerfile +++ b/veadk/cloud/harness_app/Dockerfile @@ -17,7 +17,7 @@ FROM python:3.12-slim WORKDIR /app # `[extensions]` pulls llama-index / redis / opensearch, required when the -# KNOWLEDGEBASE_TYPE or LONGTERM_MEM_TYPE env vars enable those components. +# KNOWLEDGE_BASE_TYPE or LONG_TERM_MEMORY_TYPE env vars enable those components. # (Viking / MySQL / PostgreSQL backends are already in the base dependencies.) RUN apt-get update && apt-get install -y --no-install-recommends git && \ pip3 install --no-cache-dir "veadk-python[extensions] @ git+https://github.com/volcengine/veadk-python.git" && \ diff --git a/veadk/cloud/harness_app/agent.py b/veadk/cloud/harness_app/agent.py index f78a3d96..1800a158 100644 --- a/veadk/cloud/harness_app/agent.py +++ b/veadk/cloud/harness_app/agent.py @@ -20,16 +20,16 @@ memory backend defaults to ``local``. Environment variables: - MODEL_AGENT_NAME Reasoning model name. Default: VeADK default model. - SYSTEM_PROMPT Agent instruction. Default: VeADK default instruction. - TOOLS Comma-separated built-in tool names, e.g. "web_search,link_reader". - SKILLS Comma-separated skill names, e.g. "data-visualization-cloud,...". - RUNTIME Agent runtime backend: "adk" (default) or "codex". - APP_NAME App/index name for the knowledge base and long-term memory. - Default: "harness_app". - KNOWLEDGEBASE_TYPE Knowledge base backend (e.g. "viking"). Unset disables it. - LONGTERM_MEM_TYPE Long-term memory backend (e.g. "viking"). Unset disables it. - SHORTTERM_MEM_TYPE Short-term memory backend (e.g. "sqlite"). Default: "local". + MODEL_NAME Reasoning model name. Default: VeADK default model. + SYSTEM_PROMPT Agent instruction. Default: VeADK default instruction. + TOOLS Comma-separated built-in tool names, e.g. "web_search,link_reader". + SKILLS Comma-separated skill names, e.g. "data-visualization-cloud,...". + RUNTIME Agent runtime backend: "adk" (default) or "codex". + HARNESS_NAME App/index name for the knowledge base and long-term memory + (also the served harness name). Default: "harness_app". + KNOWLEDGE_BASE_TYPE Knowledge base backend (e.g. "viking"). Unset disables it. + LONG_TERM_MEMORY_TYPE Long-term memory backend (e.g. "viking"). Unset disables it. + SHORT_TERM_MEMORY_TYPE Short-term memory backend (e.g. "sqlite"). Default: "local". """ from veadk.cloud.harness_app.utils import init_harness_agent diff --git a/veadk/cloud/harness_app/env_mapping.py b/veadk/cloud/harness_app/env_mapping.py new file mode 100644 index 00000000..67c01f27 --- /dev/null +++ b/veadk/cloud/harness_app/env_mapping.py @@ -0,0 +1,165 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Convert a layered ``harness.yaml`` into the env vars the runtime reads. + +``harness.yaml`` keeps each component self-contained — a component's backend +``type`` and its connection params live together, which is the most readable +layout for users:: + + long_term_memory: + type: viking + project: my-project + region: cn-beijing + +Two kinds of fields are converted differently: + +* **Everything except the component sections** (``harness_name``, ``model``, + ``tools``, ``skills``, ``system_prompt``, ``runtime``) is flattened with VeADK's + own :func:`veadk.utils.misc.flatten_dict` (the flattener ``set_envs`` uses for + ``config.yaml``): nested keys joined with ``_``, then upper-cased, lists + comma-joined. So ``model: {name: x}`` -> ``MODEL_NAME``, ``tools: [a, b]`` -> + ``TOOLS``. +* **Component sections** (``knowledge_base`` / ``long_term_memory`` / + ``short_term_memory``): ``type`` becomes the harness selector env, and the + remaining connection params are mapped to the VeADK env vars the backend + actually reads via :data:`BACKEND_ENV` — these can't be derived by a generic + flatten (a Viking memory's ``project`` must become ``DATABASE_VIKING_PROJECT``, + read by :class:`veadk.configs.database_configs.VikingKnowledgebaseConfig`, not + ``LONG_TERM_MEMORY_PROJECT``). + +Note: VeADK keeps one ``DATABASE__*`` config per backend, so two +components using the same backend share those vars (e.g. a Viking knowledge base +and a Viking long-term memory). +""" + +from typing import Any + +from veadk.utils.misc import flatten_dict + +# Component section -> the harness selector env naming its backend ``type`` +# (read by :func:`veadk.cloud.harness_app.utils.config_from_env`). +COMPONENT_TYPE_ENV: dict[str, str] = { + "knowledge_base": "KNOWLEDGE_BASE_TYPE", + "long_term_memory": "LONG_TERM_MEMORY_TYPE", + "short_term_memory": "SHORT_TERM_MEMORY_TYPE", +} + +# Backend ``type`` -> {harness connection param: VeADK env var}. Mirrors the +# pydantic-settings env prefixes in :mod:`veadk.configs.database_configs`; +# credentials map to the shared top-level ``VOLCENGINE_*`` vars. Backends with no +# connection params map to an empty dict (so a stray param fast-fails as a typo). +BACKEND_ENV: dict[str, dict[str, str]] = { + "viking": { + "project": "DATABASE_VIKING_PROJECT", + "region": "DATABASE_VIKING_REGION", + "access_key": "VOLCENGINE_ACCESS_KEY", + "secret_key": "VOLCENGINE_SECRET_KEY", + }, + "redis": { + "host": "DATABASE_REDIS_HOST", + "port": "DATABASE_REDIS_PORT", + "username": "DATABASE_REDIS_USERNAME", + "password": "DATABASE_REDIS_PASSWORD", + "db": "DATABASE_REDIS_DB", + }, + "opensearch": { + "host": "DATABASE_OPENSEARCH_HOST", + "port": "DATABASE_OPENSEARCH_PORT", + "username": "DATABASE_OPENSEARCH_USERNAME", + "password": "DATABASE_OPENSEARCH_PASSWORD", + "use_ssl": "DATABASE_OPENSEARCH_USE_SSL", + "cert_path": "DATABASE_OPENSEARCH_CERT_PATH", + "secret_token": "DATABASE_OPENSEARCH_SECRET_TOKEN", + }, + "mysql": { + "host": "DATABASE_MYSQL_HOST", + "user": "DATABASE_MYSQL_USER", + "password": "DATABASE_MYSQL_PASSWORD", + "database": "DATABASE_MYSQL_DATABASE", + "charset": "DATABASE_MYSQL_CHARSET", + }, + "postgresql": { + "host": "DATABASE_POSTGRESQL_HOST", + "port": "DATABASE_POSTGRESQL_PORT", + "user": "DATABASE_POSTGRESQL_USER", + "password": "DATABASE_POSTGRESQL_PASSWORD", + "database": "DATABASE_POSTGRESQL_DATABASE", + }, + "mem0": { + "api_key": "DATABASE_MEM0_API_KEY", + "api_key_id": "DATABASE_MEM0_API_KEY_ID", + "project_id": "DATABASE_MEM0_PROJECT_ID", + "base_url": "DATABASE_MEM0_BASE_URL", + }, + # In-memory / file backends take no connection params. + "local": {}, + "sqlite": {}, +} + + +def _is_empty(value: Any) -> bool: + return value is None or value == "" or value == [] or value == {} + + +def _stringify(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (list, tuple)): + return ",".join(str(item).strip() for item in value if str(item).strip()) + return str(value) + + +def to_runtime_env(spec: dict[str, Any]) -> dict[str, str]: + """Convert a parsed ``harness.yaml`` into the VeADK runtime env var dict. + + Empty values are skipped (VeADK falls back to its own defaults). An unknown + backend ``type`` or connection param raises ``ValueError`` (fast-fail on a + typo rather than silently dropping config). + """ + env: dict[str, str] = {} + + # Non-component fields: reuse VeADK's flatten_dict (same as config.yaml). + rest = {k: v for k, v in spec.items() if k not in COMPONENT_TYPE_ENV} + for key, value in flatten_dict(rest).items(): + if _is_empty(value): + continue + env[key.upper()] = _stringify(value) + + # Component sections: `type` selector + backend-specific connection params. + for component, type_env in COMPONENT_TYPE_ENV.items(): + section: dict[str, Any] = spec.get(component) or {} + if _is_empty(section.get("type")): + continue + backend = str(section["type"]) + env[type_env] = backend + + params = BACKEND_ENV.get(backend) + if params is None: + raise ValueError( + f"Unknown backend type '{backend}' for '{component}'. " + f"Known: {sorted(BACKEND_ENV)}" + ) + for param, value in section.items(): + if param == "type" or _is_empty(value): + continue + env_name = params.get(param) + if env_name is None: + raise ValueError( + f"Unknown param '{param}' for {component} backend '{backend}'. " + f"Known: {sorted(params)}" + ) + env[env_name] = _stringify(value) + + return env diff --git a/veadk/cloud/harness_app/utils.py b/veadk/cloud/harness_app/utils.py index 24717e9c..90344630 100644 --- a/veadk/cloud/harness_app/utils.py +++ b/veadk/cloud/harness_app/utils.py @@ -66,15 +66,15 @@ # populated via its "name" alias. Only variables that are set are passed, so the # model's own defaults apply to everything else. _ENV_FIELDS = { - "model_name": "MODEL_AGENT_NAME", + "model_name": "MODEL_NAME", "tools": "TOOLS", "skills": "SKILLS", "system_prompt": "SYSTEM_PROMPT", "runtime": "RUNTIME", - "name": "APP_NAME", - "knowledgebase_type": "KNOWLEDGEBASE_TYPE", - "longterm_memory_type": "LONGTERM_MEM_TYPE", - "shortterm_memory_type": "SHORTTERM_MEM_TYPE", + "name": "HARNESS_NAME", + "knowledgebase_type": "KNOWLEDGE_BASE_TYPE", + "longterm_memory_type": "LONG_TERM_MEMORY_TYPE", + "shortterm_memory_type": "SHORT_TERM_MEMORY_TYPE", } From 98abdb837fa52760df5f36b7f01750644cee18a5 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 17:38:26 +0800 Subject: [PATCH 04/15] feat(harness): full-commented harness.yaml template; deploy prints apikey + invoke cmd; add --path takes a dir - create's harness.yaml now documents every component's supported backends and their connection params as comments (uncomment for your chosen type) - deploy surfaces the created runtime's id / endpoint / api key and a ready-to-run invoke command (from the deploy result's config_updates) - harness add --path now takes the harness directory (consistent with deploy); setting a component type preserves its existing connection params --- veadk/cli/cli_harness.py | 125 ++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 22 deletions(-) diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index 5675a946..69c99793 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -47,45 +47,101 @@ # `flatten`): `model.name` -> MODEL_NAME, `knowledge_base.type` -> # KNOWLEDGE_BASE_TYPE, etc. Empty values are skipped on flatten. _HARNESS_YAML = """\ -# VeADK harness configuration. `veadk harness deploy` flattens this file into the -# runtime's environment variables (nested keys joined with `_` and upper-cased; -# lists become comma-separated; empty values are skipped). +# ============================================================================= +# VeADK harness configuration. +# +# `veadk harness deploy` converts this file into the runtime's environment +# variables: top-level fields and `model` are flattened (model.name -> MODEL_NAME, +# tools -> TOOLS, ...); each component's `type` selects its backend, and the +# component's other params map to the VeADK env vars that backend reads (e.g. +# viking `project` -> DATABASE_VIKING_PROJECT). Empty values are skipped, so +# VeADK falls back to its own defaults. +# +# Fill this in with `veadk harness add ...` or by editing the file. For a +# component, uncomment the params under the backend you set as `type`. +# ============================================================================= -# Logical harness name; also the runtime name and the knowledge-base / long-term -# memory index name. Defaults to "default" when empty. +# Logical harness name; also the AgentKit runtime name and the knowledge-base / +# long-term-memory index name. Defaults to "default" when empty. -> HARNESS_NAME harness_name: "" -# Reasoning model. On the AgentKit runtime, Ark auth is resolved from the -# runtime's IAM role, so only the model name is needed here. Extra model -# settings can be added under this section in the future. +# Reasoning model. Only the name is needed; on the AgentKit runtime Ark auth is +# resolved from the runtime's IAM role. -> MODEL_NAME model: name: "" -# Comma-flattened built-in tool names, e.g. [web_search, web_fetch]. +# Built-in tool names. -> TOOLS e.g. [web_search, link_reader] tools: [] -# Skill hub names, e.g. [clawhub/lgwventrue/system-file-handler]. +# Skill hub names. -> SKILLS e.g. [clawhub/foo/bar] skills: [] -# System prompt / instruction. Empty uses the VeADK default instruction. +# Agent instruction. Empty uses the VeADK default. -> SYSTEM_PROMPT system_prompt: "" -# Agent runtime backend: "adk" (default) or "codex". "codex" requires the -# optional codex extra installed on the server image. +# Agent runtime backend: adk (default) | codex. -> RUNTIME runtime: adk -# Knowledge base backend (e.g. "viking"). Empty disables it. Extra backend -# settings can be added under this section in the future. +# --- Knowledge base ---------------------------------------------------------- +# type: "" disables it. Supported: viking | opensearch | redis | +# tos_vector | context_search. Uncomment the params for your chosen type. knowledge_base: type: "" - -# Long-term memory backend (e.g. "viking"). Empty disables it. + # -- viking -- (-> DATABASE_VIKING_*; creds from VOLCENGINE_ACCESS/SECRET_KEY) + # project: my-project + # region: cn-beijing + # -- opensearch -- (-> DATABASE_OPENSEARCH_*) + # host: 1.2.3.4 + # port: 9200 + # username: admin + # password: "" + # use_ssl: true + # -- redis -- (-> DATABASE_REDIS_*) + # host: 1.2.3.4 + # port: 6379 + # username: default + # password: "" + # db: 0 + +# --- Long-term memory -------------------------------------------------------- +# type: "" disables it. Supported: viking | opensearch | redis | mem0. long_term_memory: type: "" - -# Short-term memory backend (e.g. "local", "mysql"). Defaults to "local". + # -- viking -- (-> DATABASE_VIKING_*) + # project: my-project + # region: cn-beijing + # -- opensearch -- (-> DATABASE_OPENSEARCH_*) + # host: 1.2.3.4 + # port: 9200 + # username: admin + # password: "" + # -- redis -- (-> DATABASE_REDIS_*) + # host: 1.2.3.4 + # port: 6379 + # password: "" + # db: 0 + # -- mem0 -- (-> DATABASE_MEM0_*) + # api_key: "" + # api_key_id: "" + # project_id: "" + # base_url: https://api.mem0.ai/v1 + +# --- Short-term memory (session store) --------------------------------------- +# type: local (default) | sqlite | mysql | postgresql. short_term_memory: type: local + # -- mysql -- (-> DATABASE_MYSQL_*) + # host: 1.2.3.4 + # user: root + # password: "" + # database: harness + # charset: utf8 + # -- postgresql -- (-> DATABASE_POSTGRESQL_*) + # host: 1.2.3.4 + # port: 5432 + # user: postgres + # password: "" + # database: harness """ # `.env.example` carries ONLY deploy credentials. All model / agent config lives @@ -440,10 +496,35 @@ def deploy( raise click.ClickException(f"Harness deploy failed: {result.error}") deploy_result = result.deploy_result - endpoint = deploy_result.endpoint_url if deploy_result else None + # The AgentKit runner records the created runtime's id / endpoint / api key in + # the deploy result's config_updates (key auth). Surface them so the user can + # invoke immediately. + updates = ( + deploy_result.config_updates.updates + if deploy_result and deploy_result.config_updates + else {} + ) + endpoint = (deploy_result.endpoint_url if deploy_result else None) or updates.get( + "runtime_endpoint" + ) + apikey = updates.get("runtime_apikey") + runtime_id = updates.get("runtime_id") + + lines = [f"Harness runtime deployed: name={runtime_name}"] + if runtime_id: + lines.append(f"Runtime id: {runtime_id}") + lines.append(f"Endpoint: {endpoint or '(see AgentKit console)'}") + if apikey: + lines.append(f"API key: {apikey}") + if endpoint and apikey: + lines.append("") + lines.append("Invoke it with:") + lines.append( + f' veadk harness invoke "" --harness {runtime_name} ' + f'--url "{endpoint}" --key "{apikey}"' + ) click.secho( - f"Harness runtime deployed: name={runtime_name}\n" - f"Endpoint: {endpoint or '(see AgentKit console)'}", + "\n".join(lines), fg="green", ) From 30c9935481d1abde7e1ee1699ae344edaad77fe4 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 17:40:34 +0800 Subject: [PATCH 05/15] fix(harness): read deploy runtime apikey/endpoint from deploy_result.metadata --- veadk/cli/cli_harness.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index 69c99793..1764724a 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -496,19 +496,13 @@ def deploy( raise click.ClickException(f"Harness deploy failed: {result.error}") deploy_result = result.deploy_result - # The AgentKit runner records the created runtime's id / endpoint / api key in - # the deploy result's config_updates (key auth). Surface them so the user can - # invoke immediately. - updates = ( - deploy_result.config_updates.updates - if deploy_result and deploy_result.config_updates - else {} - ) - endpoint = (deploy_result.endpoint_url if deploy_result else None) or updates.get( - "runtime_endpoint" - ) - apikey = updates.get("runtime_apikey") - runtime_id = updates.get("runtime_id") + # The AgentKit runner returns the created runtime's id / endpoint / api key in + # the deploy result's metadata (key auth). Surface them so the user can invoke + # immediately. + meta = deploy_result.metadata if (deploy_result and deploy_result.metadata) else {} + endpoint = deploy_result.endpoint_url if deploy_result else None + apikey = meta.get("runtime_apikey") + runtime_id = meta.get("runtime_id") lines = [f"Harness runtime deployed: name={runtime_name}"] if runtime_id: From f336ffc80cb61ad7103354d687e44f14eccdec33 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 17:48:26 +0800 Subject: [PATCH 06/15] feat(harness): 'harness add --set KEY=VALUE' writes nested params into harness.yaml Lets users fill backend connection params from the CLI, e.g. veadk harness add --long-term-memory-type viking \ --set long_term_memory.project=my-proj --set long_term_memory.region=cn-beijing Values are parsed as YAML scalars (ints/bools stay typed); deploy maps them to the veadk DATABASE_* env vars. --- veadk/cli/cli_harness.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index 1764724a..c31e0026 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -313,6 +313,14 @@ def _append_dedup(data: dict, key: str, values: tuple[str, ...]) -> None: @click.option( "--short-term-memory-type", default=None, help="Short-term memory backend." ) +@click.option( + "--set", + "set_params", + multiple=True, + metavar="KEY=VALUE", + help="Set any nested field via a dotted path (repeatable), e.g. " + "--set long_term_memory.project=my-proj --set knowledge_base.region=cn-beijing.", +) @click.option( "--path", default=".", @@ -328,13 +336,16 @@ def add( knowledge_base_type: str | None, long_term_memory_type: str | None, short_term_memory_type: str | None, + set_params: tuple[str, ...], path: str, ) -> None: """Write agent parameters into `harness.yaml`. - Scalar options SET their value; `--tool` / `--skill` are repeatable and - APPEND to the existing lists (deduped). Operates on `./harness.yaml` unless - `--path` is given; fast-fails when the file is missing. + Scalar options SET their value; `--tool` / `--skill` are repeatable and APPEND + to the lists (deduped). `--set KEY=VALUE` sets any nested field by dotted path + (e.g. a backend's connection params: `--set long_term_memory.project=my-proj`); + values are parsed as YAML scalars (so `6379` is an int, `true` a bool). + Operates on `/harness.yaml`; fast-fails when the file is missing. """ yaml_path = Path(path).resolve() / "harness.yaml" data = _load_harness_yaml(yaml_path) @@ -370,6 +381,23 @@ def add( if skills: _append_dedup(data, "skills", skills) + # Generic nested setter, e.g. "long_term_memory.project=my-proj". Values are + # parsed as YAML scalars so ints/bools stay typed. Applied last so it can fill + # connection params under the component sections set above. + for item in set_params: + dotted, sep, raw = item.partition("=") + keys = [k for k in dotted.strip().split(".") if k] + if not sep or not keys: + raise click.ClickException(f"Invalid --set '{item}'; expected KEY=VALUE.") + target = data + for key in keys[:-1]: + child = target.get(key) + if not isinstance(child, dict): + child = {} + target[key] = child + target = child + target[keys[-1]] = yaml.safe_load(raw) + yaml_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True)) click.secho(f"Updated {yaml_path}", fg="green") From 76d1dbd6a93edbd7f77c25c21c114ac865a2e58b Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 17:53:27 +0800 Subject: [PATCH 07/15] feat(harness): explicit per-component connection flags for 'harness add' Replace the generic '--set KEY=VALUE' with explicit flags in the same style as '--short-term-memory-type', e.g. --long-term-memory-project, --short-term-memory-host. Flags are auto-generated from env_mapping.COMPONENT_BACKENDS so they stay in sync with the backends; values are written under the matching component section and mapped to the veadk DATABASE_* env vars on deploy. --- veadk/cli/cli_harness.py | 77 ++++++++++++++++---------- veadk/cloud/harness_app/env_mapping.py | 28 ++++++++++ 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index c31e0026..1da1432f 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -35,7 +35,11 @@ import click import yaml -from veadk.cloud.harness_app.env_mapping import to_runtime_env +from veadk.cloud.harness_app.env_mapping import ( + COMPONENT_TYPE_ENV, + component_connection_params, + to_runtime_env, +) from veadk.cloud.harness_app.types import HarnessOverrides # Default harness/runtime name when `harness_name` is unset in `harness.yaml` @@ -286,6 +290,30 @@ def _append_dedup(data: dict, key: str, values: tuple[str, ...]) -> None: data[key] = existing +def _conn_dest(component: str, param: str) -> str: + """Click dest for a connection flag, e.g. ('long_term_memory','project').""" + return f"conn__{component}__{param}" + + +def _connection_options(func): + """Attach one explicit ``---`` flag per backend connection param. + + Generated from :data:`env_mapping.COMPONENT_BACKENDS` so the flags stay in sync + with the backends; each lands in the command's ``**connection`` kwargs. + """ + for component in reversed(list(COMPONENT_TYPE_ENV)): + label = component.replace("_", " ") + for param in reversed(component_connection_params(component)): + flag = f"--{component.replace('_', '-')}-{param.replace('_', '-')}" + func = click.option( + flag, + _conn_dest(component, param), + default=None, + help=f"{label} `{param}` (used when its type needs it).", + )(func) + return func + + @harness.command("add") @click.option("--harness-name", default=None, help="Logical harness / runtime name.") @click.option("--model-name", default=None, help="Reasoning model name.") @@ -313,14 +341,7 @@ def _append_dedup(data: dict, key: str, values: tuple[str, ...]) -> None: @click.option( "--short-term-memory-type", default=None, help="Short-term memory backend." ) -@click.option( - "--set", - "set_params", - multiple=True, - metavar="KEY=VALUE", - help="Set any nested field via a dotted path (repeatable), e.g. " - "--set long_term_memory.project=my-proj --set knowledge_base.region=cn-beijing.", -) +@_connection_options @click.option( "--path", default=".", @@ -336,16 +357,16 @@ def add( knowledge_base_type: str | None, long_term_memory_type: str | None, short_term_memory_type: str | None, - set_params: tuple[str, ...], path: str, + **connection: str | None, ) -> None: """Write agent parameters into `harness.yaml`. Scalar options SET their value; `--tool` / `--skill` are repeatable and APPEND - to the lists (deduped). `--set KEY=VALUE` sets any nested field by dotted path - (e.g. a backend's connection params: `--set long_term_memory.project=my-proj`); - values are parsed as YAML scalars (so `6379` is an int, `true` a bool). - Operates on `/harness.yaml`; fast-fails when the file is missing. + to the lists (deduped). Each backend connection param has its own flag, e.g. + `--long-term-memory-project`, `--short-term-memory-host` (see `--help`), which + is written under the matching component section. Operates on + `/harness.yaml`; fast-fails when the file is missing. """ yaml_path = Path(path).resolve() / "harness.yaml" data = _load_harness_yaml(yaml_path) @@ -381,22 +402,18 @@ def add( if skills: _append_dedup(data, "skills", skills) - # Generic nested setter, e.g. "long_term_memory.project=my-proj". Values are - # parsed as YAML scalars so ints/bools stay typed. Applied last so it can fill - # connection params under the component sections set above. - for item in set_params: - dotted, sep, raw = item.partition("=") - keys = [k for k in dotted.strip().split(".") if k] - if not sep or not keys: - raise click.ClickException(f"Invalid --set '{item}'; expected KEY=VALUE.") - target = data - for key in keys[:-1]: - child = target.get(key) - if not isinstance(child, dict): - child = {} - target[key] = child - target = child - target[keys[-1]] = yaml.safe_load(raw) + # Connection params (e.g. --long-term-memory-project) land under their + # component section, alongside the `type` set above. + for component in COMPONENT_TYPE_ENV: + for param in component_connection_params(component): + value = connection.get(_conn_dest(component, param)) + if value is None: + continue + section = data.get(component) + if not isinstance(section, dict): + section = {} + data[component] = section + section[param] = value yaml_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True)) click.secho(f"Updated {yaml_path}", fg="green") diff --git a/veadk/cloud/harness_app/env_mapping.py b/veadk/cloud/harness_app/env_mapping.py index 67c01f27..c3eefc57 100644 --- a/veadk/cloud/harness_app/env_mapping.py +++ b/veadk/cloud/harness_app/env_mapping.py @@ -109,6 +109,34 @@ } +# Backends each component supports (drives the `veadk harness add` connection +# flags and lets a component offer only its relevant params). Backends with no +# connection params (local / sqlite / tos_vector / context_search) are omitted. +COMPONENT_BACKENDS: dict[str, list[str]] = { + "knowledge_base": ["viking", "opensearch", "redis"], + "long_term_memory": ["viking", "opensearch", "redis", "mem0"], + "short_term_memory": ["mysql", "postgresql"], +} + +# Credentials come from the shared top-level VOLCENGINE_* vars (the deploy `.env`), +# not from per-component CLI flags. +_CREDENTIAL_PARAMS = frozenset({"access_key", "secret_key"}) + + +def component_connection_params(component: str) -> list[str]: + """Ordered, de-duplicated connection-param names a component's backends accept. + + Used by ``veadk harness add`` to generate one explicit flag per param + (e.g. ``--long-term-memory-project``). Credential params are excluded. + """ + params: dict[str, None] = {} + for backend in COMPONENT_BACKENDS.get(component, []): + for param in BACKEND_ENV.get(backend, {}): + if param not in _CREDENTIAL_PARAMS: + params.setdefault(param, None) + return list(params) + + def _is_empty(value: Any) -> bool: return value is None or value == "" or value == [] or value == {} From 40b59cbc3b404b4e4010dbc561c1bc9c1df0c9e8 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 18:17:40 +0800 Subject: [PATCH 08/15] feat(harness): unify add/invoke flags, knowledgebase rename, cleaner harness.yaml - add reuses invoke's HarnessOverrides-generated flags (--model-name/--tools/ --skills/--system-prompt/--runtime identical in both); --tools/--skills are comma-separated; --name (alias --harness-name) for the harness name; invoke also accepts --name - rename component knowledge_base -> knowledgebase (env KNOWLEDGEBASE_TYPE) - add writes connection params via explicit --- flags - add output: inline-style lists, and drop unset components (knowledgebase / long_term_memory); short_term_memory always shown - create's harness.yaml annotates each field with its env var + add flag - register demo tools (get_city_weather / get_location_weather) as built-ins --- veadk/cli/cli_harness.py | 205 ++++++++++++++----------- veadk/cloud/harness_app/Dockerfile | 2 +- veadk/cloud/harness_app/agent.py | 2 +- veadk/cloud/harness_app/env_mapping.py | 6 +- veadk/cloud/harness_app/utils.py | 2 +- veadk/tools/__init__.py | 3 + 6 files changed, 121 insertions(+), 99 deletions(-) diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index 1da1432f..80224ff1 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -47,9 +47,10 @@ _DEFAULT_HARNESS_NAME = "default" # Blank `harness.yaml` template written by `create`. Layered sections map to the -# flattened runtime env names consumed by `veadk.cloud.harness_app` (see -# `flatten`): `model.name` -> MODEL_NAME, `knowledge_base.type` -> -# KNOWLEDGE_BASE_TYPE, etc. Empty values are skipped on flatten. +# runtime env names consumed by `veadk.cloud.harness_app` (see env_mapping): +# `model.name` -> MODEL_NAME, `knowledgebase.type` -> KNOWLEDGEBASE_TYPE, a +# backend's params -> DATABASE__*. Each line is annotated with its env +# var and the `veadk harness add` flag that sets it. _HARNESS_YAML = """\ # ============================================================================= # VeADK harness configuration. @@ -61,46 +62,48 @@ # viking `project` -> DATABASE_VIKING_PROJECT). Empty values are skipped, so # VeADK falls back to its own defaults. # -# Fill this in with `veadk harness add ...` or by editing the file. For a +# Configure with `veadk harness add ...` or by editing this file. For a # component, uncomment the params under the backend you set as `type`. # ============================================================================= -# Logical harness name; also the AgentKit runtime name and the knowledge-base / -# long-term-memory index name. Defaults to "default" when empty. -> HARNESS_NAME +# Harness / runtime name (also the knowledgebase & long-term-memory index name). +# env: HARNESS_NAME flag: --name harness_name: "" -# Reasoning model. Only the name is needed; on the AgentKit runtime Ark auth is -# resolved from the runtime's IAM role. -> MODEL_NAME +# Reasoning model name (Ark auth comes from the runtime's IAM role on deploy). +# env: MODEL_NAME flag: --model-name model: name: "" -# Built-in tool names. -> TOOLS e.g. [web_search, link_reader] +# Built-in tool names. env: TOOLS flag: --tools (comma-separated) tools: [] -# Skill hub names. -> SKILLS e.g. [clawhub/foo/bar] +# Skill hub names. env: SKILLS flag: --skills (comma-separated) skills: [] -# Agent instruction. Empty uses the VeADK default. -> SYSTEM_PROMPT +# Agent instruction (empty = VeADK default). +# env: SYSTEM_PROMPT flag: --system-prompt system_prompt: "" -# Agent runtime backend: adk (default) | codex. -> RUNTIME +# Agent runtime backend: adk (default) | codex. +# env: RUNTIME flag: --runtime runtime: adk # --- Knowledge base ---------------------------------------------------------- -# type: "" disables it. Supported: viking | opensearch | redis | -# tos_vector | context_search. Uncomment the params for your chosen type. -knowledge_base: +# type -> env: KNOWLEDGEBASE_TYPE flag: --knowledgebase-type +# "" disables it. Supported: viking | opensearch | redis | tos_vector | context_search +knowledgebase: type: "" - # -- viking -- (-> DATABASE_VIKING_*; creds from VOLCENGINE_ACCESS/SECRET_KEY) + # -- viking -- env DATABASE_VIKING_* flags: --knowledgebase-project / --knowledgebase-region # project: my-project # region: cn-beijing - # -- opensearch -- (-> DATABASE_OPENSEARCH_*) + # -- opensearch -- env DATABASE_OPENSEARCH_* flags: --knowledgebase-host / -port / -username / -password / -use-ssl # host: 1.2.3.4 # port: 9200 # username: admin # password: "" # use_ssl: true - # -- redis -- (-> DATABASE_REDIS_*) + # -- redis -- env DATABASE_REDIS_* flags: --knowledgebase-host / -port / -username / -password / -db # host: 1.2.3.4 # port: 6379 # username: default @@ -108,39 +111,41 @@ # db: 0 # --- Long-term memory -------------------------------------------------------- -# type: "" disables it. Supported: viking | opensearch | redis | mem0. +# type -> env: LONG_TERM_MEMORY_TYPE flag: --long-term-memory-type +# "" disables it. Supported: viking | opensearch | redis | mem0 long_term_memory: type: "" - # -- viking -- (-> DATABASE_VIKING_*) + # -- viking -- env DATABASE_VIKING_* flags: --long-term-memory-project / --long-term-memory-region # project: my-project # region: cn-beijing - # -- opensearch -- (-> DATABASE_OPENSEARCH_*) + # -- opensearch -- env DATABASE_OPENSEARCH_* flags: --long-term-memory-host / -port / -username / -password # host: 1.2.3.4 # port: 9200 # username: admin # password: "" - # -- redis -- (-> DATABASE_REDIS_*) + # -- redis -- env DATABASE_REDIS_* flags: --long-term-memory-host / -port / -password / -db # host: 1.2.3.4 # port: 6379 # password: "" # db: 0 - # -- mem0 -- (-> DATABASE_MEM0_*) + # -- mem0 -- env DATABASE_MEM0_* flags: --long-term-memory-api-key / -api-key-id / -project-id / -base-url # api_key: "" # api_key_id: "" # project_id: "" # base_url: https://api.mem0.ai/v1 # --- Short-term memory (session store) --------------------------------------- -# type: local (default) | sqlite | mysql | postgresql. +# type -> env: SHORT_TERM_MEMORY_TYPE flag: --short-term-memory-type +# local (default) | sqlite | mysql | postgresql short_term_memory: type: local - # -- mysql -- (-> DATABASE_MYSQL_*) + # -- mysql -- env DATABASE_MYSQL_* flags: --short-term-memory-host / -user / -password / -database / -charset # host: 1.2.3.4 # user: root # password: "" # database: harness # charset: utf8 - # -- postgresql -- (-> DATABASE_POSTGRESQL_*) + # -- postgresql -- env DATABASE_POSTGRESQL_* flags: --short-term-memory-host / -port / -user / -password / -database # host: 1.2.3.4 # port: 5432 # user: postgres @@ -279,22 +284,36 @@ def _load_harness_yaml(path: Path) -> dict: return yaml.safe_load(path.read_text()) or {} -def _append_dedup(data: dict, key: str, values: tuple[str, ...]) -> None: - """Append ``values`` to the list at ``data[key]``, preserving order, deduped.""" - existing = data.get(key) or [] - if not isinstance(existing, list): - existing = [existing] - for value in values: - if value not in existing: - existing.append(value) - data[key] = existing - - def _conn_dest(component: str, param: str) -> str: """Click dest for a connection flag, e.g. ('long_term_memory','project').""" return f"conn__{component}__{param}" +def _is_blank(value: object) -> bool: + return value is None or value == "" or value == [] or value == {} + + +def _prune_empty(data: dict) -> None: + """Drop unset fields so `add` writes only what's configured. + + Empty scalars/lists are removed; a component section with no `type` is + dropped entirely. ``short_term_memory`` is always kept (its `local` default + is shown). + """ + for key in list(data): + if key == "short_term_memory": + continue + value = data[key] + if isinstance(value, dict): + for sub in list(value): + if _is_blank(value[sub]): + del value[sub] + if (key in COMPONENT_TYPE_ENV and not value.get("type")) or not value: + del data[key] + elif _is_blank(value): + del data[key] + + def _connection_options(func): """Attach one explicit ``---`` flag per backend connection param. @@ -314,29 +333,42 @@ def _connection_options(func): return func +def _override_options(func): + """Attach a ``--flag`` for every :class:`HarnessOverrides` field. + + Shared by ``add`` and ``invoke`` so their model / tools / skills / + system-prompt / runtime flags stay identical and in sync with the model — + adding a field to ``HarnessOverrides`` exposes the flag in both. Each flag + defaults to ``None`` (unset → not applied). + """ + for name, field in reversed(list(HarnessOverrides.model_fields.items())): + option: dict = { + "default": None, + "help": field.description or f"`{name}`.", + } + if typing.get_origin(field.annotation) is typing.Literal: + option["type"] = click.Choice( + [str(arg) for arg in typing.get_args(field.annotation)] + ) + func = click.option("--" + name.replace("_", "-"), name, **option)(func) + return func + + @harness.command("add") -@click.option("--harness-name", default=None, help="Logical harness / runtime name.") -@click.option("--model-name", default=None, help="Reasoning model name.") @click.option( - "--tool", - "tools", - multiple=True, - help="Built-in tool name to append to `tools` (repeatable).", -) -@click.option( - "--skill", - "skills", - multiple=True, - help="Skill hub name to append to `skills` (repeatable).", + "--name", + "--harness-name", + "harness_name", + default=None, + help="Logical harness / runtime name.", ) -@click.option("--system-prompt", default=None, help="System prompt / instruction.") +@_override_options @click.option( - "--runtime", - type=click.Choice(["adk", "codex"]), + "--knowledgebase-type", + "knowledgebase_type", default=None, - help="Agent runtime backend.", + help="Knowledge base backend.", ) -@click.option("--knowledge-base-type", default=None, help="Knowledge base backend.") @click.option("--long-term-memory-type", default=None, help="Long-term memory backend.") @click.option( "--short-term-memory-type", default=None, help="Short-term memory backend." @@ -349,24 +381,23 @@ def _connection_options(func): ) def add( harness_name: str | None, - model_name: str | None, - tools: tuple[str, ...], - skills: tuple[str, ...], - system_prompt: str | None, - runtime: str | None, - knowledge_base_type: str | None, + knowledgebase_type: str | None, long_term_memory_type: str | None, short_term_memory_type: str | None, path: str, + model_name: str | None, + tools: str | None, + skills: str | None, + system_prompt: str | None, + runtime: str | None, **connection: str | None, ) -> None: """Write agent parameters into `harness.yaml`. - Scalar options SET their value; `--tool` / `--skill` are repeatable and APPEND - to the lists (deduped). Each backend connection param has its own flag, e.g. - `--long-term-memory-project`, `--short-term-memory-host` (see `--help`), which - is written under the matching component section. Operates on - `/harness.yaml`; fast-fails when the file is missing. + Options SET their value; `--tools` / `--skills` take comma-separated lists. + Each backend connection param has its own flag, e.g. `--long-term-memory-project`, + `--short-term-memory-host` (see `--help`), written under the matching component + section. Operates on `/harness.yaml`; fast-fails when the file is missing. """ yaml_path = Path(path).resolve() / "harness.yaml" data = _load_harness_yaml(yaml_path) @@ -386,7 +417,7 @@ def add( # Set only the backend `type`, preserving any connection params already set # under the component section. for type_value, section_key in ( - (knowledge_base_type, "knowledge_base"), + (knowledgebase_type, "knowledgebase"), (long_term_memory_type, "long_term_memory"), (short_term_memory_type, "short_term_memory"), ): @@ -397,10 +428,10 @@ def add( section["type"] = type_value data[section_key] = section - if tools: - _append_dedup(data, "tools", tools) - if skills: - _append_dedup(data, "skills", skills) + if tools is not None: + data["tools"] = [t.strip() for t in tools.split(",") if t.strip()] + if skills is not None: + data["skills"] = [s.strip() for s in skills.split(",") if s.strip()] # Connection params (e.g. --long-term-memory-project) land under their # component section, alongside the `type` set above. @@ -415,7 +446,12 @@ def add( data[component] = section section[param] = value - yaml_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True)) + _prune_empty(data) + yaml_path.write_text( + yaml.safe_dump( + data, sort_keys=False, allow_unicode=True, default_flow_style=None + ) + ) click.secho(f"Updated {yaml_path}", fg="green") @@ -568,31 +604,14 @@ def deploy( ) -def _override_options(func): - """Attach a ``--flag`` for every :class:`HarnessOverrides` field. - - The override flags are generated from the model, so adding a field to - ``HarnessOverrides`` exposes a new CLI flag automatically — there is no second - place to update. Each flag defaults to ``None`` (unset → omitted from the - request), preserving the server's partial-override semantics. - """ - for name, field in reversed(list(HarnessOverrides.model_fields.items())): - option: dict = { - "default": None, - "help": field.description or f"Override `{name}` for this call.", - } - if typing.get_origin(field.annotation) is typing.Literal: - option["type"] = click.Choice( - [str(arg) for arg in typing.get_args(field.annotation)] - ) - func = click.option("--" + name.replace("_", "-"), name, **option)(func) - return func - - @harness.command("invoke") @click.argument("message") @click.option( - "--harness", "harness_name", required=True, help="Harness name to invoke." + "--name", + "--harness", + "harness_name", + required=True, + help="Harness name to invoke.", ) @click.option( "--user-id", "user_id", default="cli-user", help="User id for the session." diff --git a/veadk/cloud/harness_app/Dockerfile b/veadk/cloud/harness_app/Dockerfile index f1494eed..6aa549fc 100644 --- a/veadk/cloud/harness_app/Dockerfile +++ b/veadk/cloud/harness_app/Dockerfile @@ -17,7 +17,7 @@ FROM python:3.12-slim WORKDIR /app # `[extensions]` pulls llama-index / redis / opensearch, required when the -# KNOWLEDGE_BASE_TYPE or LONG_TERM_MEMORY_TYPE env vars enable those components. +# KNOWLEDGEBASE_TYPE or LONG_TERM_MEMORY_TYPE env vars enable those components. # (Viking / MySQL / PostgreSQL backends are already in the base dependencies.) RUN apt-get update && apt-get install -y --no-install-recommends git && \ pip3 install --no-cache-dir "veadk-python[extensions] @ git+https://github.com/volcengine/veadk-python.git" && \ diff --git a/veadk/cloud/harness_app/agent.py b/veadk/cloud/harness_app/agent.py index 1800a158..6222c9ba 100644 --- a/veadk/cloud/harness_app/agent.py +++ b/veadk/cloud/harness_app/agent.py @@ -27,7 +27,7 @@ RUNTIME Agent runtime backend: "adk" (default) or "codex". HARNESS_NAME App/index name for the knowledge base and long-term memory (also the served harness name). Default: "harness_app". - KNOWLEDGE_BASE_TYPE Knowledge base backend (e.g. "viking"). Unset disables it. + KNOWLEDGEBASE_TYPE Knowledge base backend (e.g. "viking"). Unset disables it. LONG_TERM_MEMORY_TYPE Long-term memory backend (e.g. "viking"). Unset disables it. SHORT_TERM_MEMORY_TYPE Short-term memory backend (e.g. "sqlite"). Default: "local". """ diff --git a/veadk/cloud/harness_app/env_mapping.py b/veadk/cloud/harness_app/env_mapping.py index c3eefc57..d76a2d69 100644 --- a/veadk/cloud/harness_app/env_mapping.py +++ b/veadk/cloud/harness_app/env_mapping.py @@ -31,7 +31,7 @@ ``config.yaml``): nested keys joined with ``_``, then upper-cased, lists comma-joined. So ``model: {name: x}`` -> ``MODEL_NAME``, ``tools: [a, b]`` -> ``TOOLS``. -* **Component sections** (``knowledge_base`` / ``long_term_memory`` / +* **Component sections** (``knowledgebase`` / ``long_term_memory`` / ``short_term_memory``): ``type`` becomes the harness selector env, and the remaining connection params are mapped to the VeADK env vars the backend actually reads via :data:`BACKEND_ENV` — these can't be derived by a generic @@ -51,7 +51,7 @@ # Component section -> the harness selector env naming its backend ``type`` # (read by :func:`veadk.cloud.harness_app.utils.config_from_env`). COMPONENT_TYPE_ENV: dict[str, str] = { - "knowledge_base": "KNOWLEDGE_BASE_TYPE", + "knowledgebase": "KNOWLEDGEBASE_TYPE", "long_term_memory": "LONG_TERM_MEMORY_TYPE", "short_term_memory": "SHORT_TERM_MEMORY_TYPE", } @@ -113,7 +113,7 @@ # flags and lets a component offer only its relevant params). Backends with no # connection params (local / sqlite / tos_vector / context_search) are omitted. COMPONENT_BACKENDS: dict[str, list[str]] = { - "knowledge_base": ["viking", "opensearch", "redis"], + "knowledgebase": ["viking", "opensearch", "redis"], "long_term_memory": ["viking", "opensearch", "redis", "mem0"], "short_term_memory": ["mysql", "postgresql"], } diff --git a/veadk/cloud/harness_app/utils.py b/veadk/cloud/harness_app/utils.py index 90344630..52388d7b 100644 --- a/veadk/cloud/harness_app/utils.py +++ b/veadk/cloud/harness_app/utils.py @@ -72,7 +72,7 @@ "system_prompt": "SYSTEM_PROMPT", "runtime": "RUNTIME", "name": "HARNESS_NAME", - "knowledgebase_type": "KNOWLEDGE_BASE_TYPE", + "knowledgebase_type": "KNOWLEDGEBASE_TYPE", "longterm_memory_type": "LONG_TERM_MEMORY_TYPE", "shortterm_memory_type": "SHORT_TERM_MEMORY_TYPE", } diff --git a/veadk/tools/__init__.py b/veadk/tools/__init__.py index 7b3d37b9..e2b5a78e 100644 --- a/veadk/tools/__init__.py +++ b/veadk/tools/__init__.py @@ -38,6 +38,9 @@ "image_edit": "veadk.tools.builtin_tools.image_edit:image_edit", "video_generate": "veadk.tools.builtin_tools.video_generate:video_generate", "text_to_speech": "veadk.tools.builtin_tools.tts:text_to_speech", + # Demo / example tools + "get_city_weather": "veadk.tools.demo_tools:get_city_weather", + "get_location_weather": "veadk.tools.demo_tools:get_location_weather", } From 6dbe594565268b22a4fbc30b652d5b6249830417 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 19:36:47 +0800 Subject: [PATCH 09/15] feat(harness): deploy records harness.json; invoke resolves url/key from it + --message - deploy writes/updates ./harness.json: {name: {url, key, runtime_id}} - invoke takes --name (reads url/key from harness.json), --message/-m (or positional); --url/--key optional overrides; --path for harness.json location --- veadk/cli/cli_harness.py | 83 ++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index 80224ff1..d6a8a5c1 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -500,6 +500,27 @@ def _build_agentkit_config( } +def _harness_json_path(directory: str) -> Path: + return Path(directory).resolve() / "harness.json" + + +def _load_harness_json(directory: str) -> dict: + """Load the `{name: {url, key, runtime_id}}` registry, or {} if absent.""" + path = _harness_json_path(directory) + return json.loads(path.read_text()) if path.is_file() else {} + + +def _record_harness( + directory: str, name: str, url: str, key: str, runtime_id: str +) -> Path: + """Record/replace a deployed harness's url + key + id in `harness.json`.""" + path = _harness_json_path(directory) + data = _load_harness_json(directory) + data[name] = {"url": url, "key": key, "runtime_id": runtime_id} + path.write_text(json.dumps(data, indent=2, ensure_ascii=False)) + return path + + @harness.command("deploy") @click.option("--volcengine-access-key", default=None, help="Volcengine access key.") @click.option("--volcengine-secret-key", default=None, help="Volcengine secret key.") @@ -592,11 +613,13 @@ def deploy( if apikey: lines.append(f"API key: {apikey}") if endpoint and apikey: + json_path = _record_harness( + path, runtime_name, endpoint, apikey, runtime_id or "" + ) lines.append("") - lines.append("Invoke it with:") + lines.append(f"Recorded in {json_path}. Invoke it with:") lines.append( - f' veadk harness invoke "" --harness {runtime_name} ' - f'--url "{endpoint}" --key "{apikey}"' + f' veadk harness invoke --name {runtime_name} --message ""' ) click.secho( "\n".join(lines), @@ -605,14 +628,15 @@ def deploy( @harness.command("invoke") -@click.argument("message") +@click.argument("message_arg", metavar="[MESSAGE]", required=False) @click.option( "--name", "--harness", "harness_name", required=True, - help="Harness name to invoke.", + help="Harness name; its url/key are read from harness.json unless overridden.", ) +@click.option("--message", "-m", "message_opt", default=None, help="Message to send.") @click.option( "--user-id", "user_id", default="cli-user", help="User id for the session." ) @@ -624,27 +648,56 @@ def deploy( ) @click.option( "--url", - required=True, + default=None, envvar="HARNESS_URL", - help="Harness server base URL (or set HARNESS_URL).", + help="Harness URL (default: harness.json[name], or HARNESS_URL).", ) @click.option( "--key", default=None, envvar="HARNESS_KEY", - help="Gateway API key for Bearer auth (or set HARNESS_KEY).", + help="API key for Bearer auth (default: harness.json[name], or HARNESS_KEY).", +) +@click.option( + "--path", default=".", help="Dir containing harness.json (default: current dir)." ) @_override_options -def invoke(message, harness_name, user_id, session_id, url, key, **overrides) -> None: - """Invoke a deployed harness with MESSAGE and print its output. - - Any override flag (generated from ``HarnessOverrides``) applies a once-time - override on top of the deployed agent for this single call; unset flags are - omitted, so the server keeps its configured values (memory and the knowledge - base are never overridable). +def invoke( + message_arg, + harness_name, + message_opt, + user_id, + session_id, + url, + key, + path, + **overrides, +) -> None: + """Invoke a deployed harness and print its output. + + Pass the prompt as the MESSAGE argument or via `--message`. The harness `url` + and `key` are read from `harness.json` (written by `deploy`) by `--name`, + unless given explicitly. Any override flag (generated from ``HarnessOverrides``, + e.g. `--tools`, `--system-prompt`) applies a once-time override on top of the + deployed agent for this single call; memory and the knowledge base are never + overridable. """ from veadk.cli.cli_agentkit import _harness_request + message = message_opt or message_arg + if not message: + raise click.ClickException("Provide a prompt (MESSAGE argument or --message).") + + if not url or not key: + record = _load_harness_json(path).get(harness_name, {}) + url = url or record.get("url") + key = key or record.get("key") + if not url: + raise click.ClickException( + f"No URL for '{harness_name}'. Deploy it first (records harness.json) " + "or pass --url/--key." + ) + body: dict = { "prompt": message, "harness_name": harness_name, From ea854dd9445fb34b230eaa34b910cbbeec8e951f Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 19:47:54 +0800 Subject: [PATCH 10/15] feat(harness): add 'veadk harness show' (configured agent params + overridable flags) --- docs/content/docs/cli/harness-cli.en.mdx | 25 +++++++++++++++++ docs/content/docs/cli/harness-cli.mdx | 18 ++++++++++++ veadk/cli/cli_harness.py | 35 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/docs/content/docs/cli/harness-cli.en.mdx b/docs/content/docs/cli/harness-cli.en.mdx index f7127014..d40298df 100644 --- a/docs/content/docs/cli/harness-cli.en.mdx +++ b/docs/content/docs/cli/harness-cli.en.mdx @@ -27,6 +27,7 @@ below). The agent is configured through a layered `harness.yaml`. | :-- | :-- | | `veadk harness create ` | Scaffold a deployable harness directory. | | `veadk harness add` | Write agent parameters into `harness.yaml`. | +| `veadk harness show` | Show the configured params and the per-invoke overridable ones. | | `veadk harness deploy` | Cloud-build the image and create an AgentKit runtime. | ### `veadk harness create` @@ -101,6 +102,30 @@ veadk harness add \ | `--short-term-memory-type` | Short-term memory backend (sets `short_term_memory.type`). | | `--path` | Path to `harness.yaml`, default `./harness.yaml`. | +### `veadk harness show` + +Print the parameters configured in `harness.yaml` plus the params that can be +overridden per call. Fast-fails when `harness.yaml` is missing. + +```bash +veadk harness show --path my-harness +``` + +It prints two sections: + +1. **Configured agent params** — the `harness.yaml` contents: `harness_name`, + `model`, `tools`, `skills`, `system_prompt`, `runtime`, and any configured + `knowledgebase` / `long_term_memory` / `short_term_memory` components. +2. **Overridable at invoke time** — each `--` derived from + `HarnessOverrides` (`--model-name`, `--tools`, `--skills`, `--system-prompt`, + `--runtime`) with its description. These can be overridden per call via + `veadk harness invoke ... --`; memory and the knowledge base are **not** + overridable. + +| Option | Description | +| :-- | :-- | +| `--path` | Harness directory containing `harness.yaml`, default `.`. | + ### `veadk harness deploy` Fill in deploy credentials, then from inside the directory: diff --git a/docs/content/docs/cli/harness-cli.mdx b/docs/content/docs/cli/harness-cli.mdx index 8299bf66..7e0e20c2 100644 --- a/docs/content/docs/cli/harness-cli.mdx +++ b/docs/content/docs/cli/harness-cli.mdx @@ -19,6 +19,7 @@ description: "通过命令行注册并调用部署在 Harness server 上的 harn | :-- | :-- | | `veadk harness create ` | 生成一个可部署的 harness 目录。 | | `veadk harness add` | 将智能体参数写入 `harness.yaml`。 | +| `veadk harness show` | 展示已配置参数以及可在调用时覆盖的参数。 | | `veadk harness deploy` | 云端构建镜像并创建一个 AgentKit runtime。 | 智能体通过分层的 `harness.yaml` 进行配置。 @@ -87,6 +88,23 @@ veadk harness add \ | `--short-term-memory-type` | 短期记忆后端(设置 `short_term_memory.type`)。 | | `--path` | `harness.yaml` 路径,默认 `./harness.yaml`。 | +### `veadk harness show` + +打印 `harness.yaml` 中已配置的参数,以及可在每次调用时覆盖的参数。当 `harness.yaml` 不存在时快速失败。 + +```bash +veadk harness show --path my-harness +``` + +该命令打印两个分段: + +1. **已配置的智能体参数** —— `harness.yaml` 的内容:`harness_name`、`model`、`tools`、`skills`、`system_prompt`、`runtime`,以及已配置的 `knowledgebase` / `long_term_memory` / `short_term_memory` 组件。 +2. **可在调用时覆盖的参数** —— 由 `HarnessOverrides` 派生的每个 `--`(`--model-name`、`--tools`、`--skills`、`--system-prompt`、`--runtime`)及其说明。这些参数可通过 `veadk harness invoke ... --` 按次覆盖;记忆与知识库**不可**覆盖。 + +| 选项 | 说明 | +| :-- | :-- | +| `--path` | 包含 `harness.yaml` 的 harness 目录,默认 `.`。 | + ### `veadk harness deploy` 填好部署凭证后,在该目录内执行: diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index d6a8a5c1..d70b7f7b 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -455,6 +455,41 @@ def add( click.secho(f"Updated {yaml_path}", fg="green") +@harness.command("show") +@click.option( + "--path", + default=".", + help="Harness directory containing harness.yaml (default: current dir).", +) +def show(path: str) -> None: + """Show the configured agent params and the per-invoke overridable params. + + Reads `/harness.yaml` (fast-fails when missing) and prints (1) the + currently configured agent parameters and components, and (2) the params that + can be overridden per call via `veadk harness invoke`. + """ + yaml_path = Path(path).resolve() / "harness.yaml" + data = _load_harness_yaml(yaml_path) + + click.secho(f"Configured agent params ({yaml_path}):", fg="green", bold=True) + click.echo( + yaml.safe_dump( + data, sort_keys=False, allow_unicode=True, default_flow_style=None + ).rstrip() + ) + + click.echo("") + click.secho("Overridable at invoke time:", fg="green", bold=True) + for name, field in HarnessOverrides.model_fields.items(): + flag = "--" + name.replace("_", "-") + click.echo(f" {flag}: {field.description or name}") + click.echo("") + click.echo( + "Override per call via `veadk harness invoke ... --`. " + "Memory and knowledgebase are NOT overridable." + ) + + def _build_agentkit_config( runtime_name: str, region: str, envs: dict[str, str] ) -> dict: From c512d9cfee8f04480117884faeb4f31a5e62f0d1 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 20:35:32 +0800 Subject: [PATCH 11/15] docs(harness): rewrite CLI reference for current create/add/show/deploy/invoke flow The reference still described the removed server-side `/harness/add` registration and the old `--harness/--url/--key` invoke design. Rewrite it around the current `veadk harness` flow: layered harness.yaml (knowledgebase/KNOWLEDGEBASE_TYPE), comma-separated --tools/--skills, per-component connection flags, deploy writing harness.json, and invoke resolving url/key by --name. --- docs/content/docs/cli/harness-cli.en.mdx | 234 +++++++++-------------- docs/content/docs/cli/harness-cli.mdx | 190 +++++++++--------- 2 files changed, 179 insertions(+), 245 deletions(-) diff --git a/docs/content/docs/cli/harness-cli.en.mdx b/docs/content/docs/cli/harness-cli.en.mdx index d40298df..a22dcdc6 100644 --- a/docs/content/docs/cli/harness-cli.en.mdx +++ b/docs/content/docs/cli/harness-cli.en.mdx @@ -1,111 +1,113 @@ --- title: "Harness Commands" -description: "Register and invoke harnesses (agent specs) on a deployed Harness server from the CLI." +description: "Scaffold, configure, deploy, and invoke a VeADK Harness server." --- -`veadk agentkit harness` is a thin set of HTTP client commands for a deployed -**Harness server** (`veadk.cloud.harness_app`). +`veadk harness` is a group of commands to **scaffold, configure, deploy, and invoke** a VeADK **Harness server** (`veadk.cloud.harness_app`). -A *harness* is a named agent spec — **model + system prompt + tools + skills**. -The Harness server exposes `/harness/add` and `/harness/invoke`, letting you -register and invoke harnesses at runtime without redeploying. +A *harness* is an agent specification — **model + system prompt + tools + skills**, plus a knowledge base and long/short-term memory bound at creation time. The whole spec is described by a layered `harness.yaml`; `deploy` flattens it into the runtime's environment variables, the Harness server assembles an agent from them at startup, and serves it over `POST /harness/invoke`. - Before you start, deploy the Harness server to Volcengine AgentKit (see - [Deploying the Harness server](#deploying-the-harness-server) below, or the - [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit) - example) and grab its **endpoint URL** and gateway **API key**. + Deploying requires Volcengine AgentKit credentials (`VOLCENGINE_ACCESS_KEY` / `VOLCENGINE_SECRET_KEY`). For an end-to-end example see [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit). -## Deploying the Harness server - -The top-level `veadk harness` group scaffolds, configures, and deploys the -Harness server itself (this is separate from the `veadk agentkit harness` client -below). The agent is configured through a layered `harness.yaml`. +## Commands at a glance | Command | Description | | :-- | :-- | | `veadk harness create ` | Scaffold a deployable harness directory. | | `veadk harness add` | Write agent parameters into `harness.yaml`. | -| `veadk harness show` | Show the configured params and the per-invoke overridable ones. | -| `veadk harness deploy` | Cloud-build the image and create an AgentKit runtime. | +| `veadk harness show` | Show configured params and the per-invoke overridable params. | +| `veadk harness deploy` | Cloud-build the image and create an AgentKit runtime (no local Docker). | +| `veadk harness invoke` | Invoke a deployed harness and print its output. | + +Typical flow: **`create` → `add` → `deploy` → `invoke`**. -### `veadk harness create` +## `veadk harness create` -Create a deployment directory: +Scaffold a deployment directory: ```bash veadk harness create my-harness ``` -This writes: +It writes: -- `harness.yaml` — the agent configuration template (see below). -- `.env.example` — Volcengine deploy credentials only (`VOLCENGINE_ACCESS_KEY`, - `VOLCENGINE_SECRET_KEY`, optional `VOLCENGINE_REGION`). Model/agent config - lives in `harness.yaml`. +- `harness.yaml` — the agent config template (see below). +- `.env.example` — Volcengine deploy credentials only (`VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY`, and optional `VOLCENGINE_REGION`). Model/agent config lives in `harness.yaml`. - `Dockerfile` — builds the harness server image. -- `README.md` — quickstart. +- `README.md` — quick-start notes. -#### `harness.yaml` +### `harness.yaml` -A layered configuration file. `deploy` flattens it into the runtime's -environment variables: nested keys are joined with `_` and upper-cased, lists -become comma-separated strings, and empty values are skipped. +A layered, self-contained-per-component config. `deploy` converts it into the runtime's environment variables: top-level fields and `model` are flattened (`model.name` → `MODEL_NAME`, `tools` → `TOOLS`, …) with lists comma-joined; each component's `type` selects its backend, and the component's other params map to the VeADK env vars that backend actually reads (e.g. viking `project` → `DATABASE_VIKING_PROJECT`). Empty values are skipped, so VeADK falls back to its own defaults. ```yaml -harness_name: "" # -> HARNESS_NAME (runtime + KB/memory index name) +harness_name: "" # -> HARNESS_NAME (runtime name + knowledgebase/long-term-memory index name) model: - name: "" # -> MODEL_NAME + name: "" # -> MODEL_NAME (Ark auth is resolved by the runtime's IAM role after deploy) tools: [] # -> TOOLS (comma-joined) skills: [] # -> SKILLS (comma-joined) -system_prompt: "" # -> SYSTEM_PROMPT +system_prompt: "" # -> SYSTEM_PROMPT (empty = VeADK default) runtime: adk # -> RUNTIME ("adk" or "codex") -knowledge_base: - type: "" # -> KNOWLEDGE_BASE_TYPE +knowledgebase: + type: "" # -> KNOWLEDGEBASE_TYPE (empty = not created) long_term_memory: - type: "" # -> LONG_TERM_MEMORY_TYPE + type: "" # -> LONG_TERM_MEMORY_TYPE (empty = not created) short_term_memory: type: local # -> SHORT_TERM_MEMORY_TYPE ``` -On the AgentKit runtime, Ark auth is resolved from the runtime's IAM role, so no -model API key is configured here. Future parameters (extra memory/KB settings) -can be added under each section. +Under each component, uncomment the params for the backend you set as `type` (the template annotates the env var and CLI flag for every field). On an AgentKit runtime Ark auth is resolved by the IAM role, so the model needs no API key here. -### `veadk harness add` +## `veadk harness add` -Write agent parameters into `./harness.yaml` (or `--path`). Scalar options SET -their value; `--tool` / `--skill` are repeatable and APPEND to the lists -(deduped). Fast-fails when `harness.yaml` is missing. +Write agent parameters into `./harness.yaml` (or the directory given by `--path`). Fails fast if `harness.yaml` does not exist. ```bash cd my-harness veadk harness add \ - --harness-name research-agent \ + --name research-agent \ --model-name doubao-seed-1-6-250615 \ --system-prompt "You are a research assistant." \ - --tool web_search --tool web_fetch \ + --tools web_search,web_fetch \ --runtime adk ``` | Option | Description | | :-- | :-- | -| `--harness-name` | Logical harness / runtime name (sets `harness_name`). | +| `--name` / `--harness-name` | Logical harness / runtime name (sets `harness_name`). | | `--model-name` | Reasoning model name (sets `model.name`). | -| `--tool` | Built-in tool name to append to `tools` (repeatable). | -| `--skill` | Skill hub name to append to `skills` (repeatable). | +| `--tools` | Comma-separated built-in tool names, e.g. `web_search,web_fetch` (sets `tools`). | +| `--skills` | Comma-separated skill hub names (sets `skills`). | | `--system-prompt` | System prompt / instruction. | -| `--runtime` | Agent runtime backend, `adk` or `codex`. | -| `--knowledge-base-type` | Knowledge base backend (sets `knowledge_base.type`). | +| `--runtime` | Agent runtime, `adk` (default) or `codex`. | +| `--knowledgebase-type` | Knowledge base backend (sets `knowledgebase.type`). | | `--long-term-memory-type` | Long-term memory backend (sets `long_term_memory.type`). | | `--short-term-memory-type` | Short-term memory backend (sets `short_term_memory.type`). | -| `--path` | Path to `harness.yaml`, default `./harness.yaml`. | +| `--path` | Directory containing `harness.yaml`, default `.`. | + +#### Component connection params + +Each component also auto-generates `---` connection flags for the backends it supports, e.g.: + +```bash +veadk harness add --name kb-agent \ + --knowledgebase-type viking \ + --knowledgebase-project my-project --knowledgebase-region cn-beijing +``` + +| Component prefix | Available flags (depend on backend) | +| :-- | :-- | +| `--knowledgebase-*` | `project` / `region` (viking); `host` / `port` / `username` / `password` / `use-ssl` / `cert-path` / `secret-token` (opensearch); `host` / `port` / `username` / `password` / `db` (redis) | +| `--long-term-memory-*` | the same per-backend params, plus `api-key` / `api-key-id` / `project-id` / `base-url` (mem0) | +| `--short-term-memory-*` | `host` / `user` / `password` / `database` / `charset` / `port` (mysql / postgresql) | + +Credentials (access key / secret key) are not passed via these flags; they reuse the `VOLCENGINE_*` values from the deploy `.env`. -### `veadk harness show` +## `veadk harness show` -Print the parameters configured in `harness.yaml` plus the params that can be -overridden per call. Fast-fails when `harness.yaml` is missing. +Print the params configured in `harness.yaml`, plus the params overridable per invocation. Fails fast if `harness.yaml` does not exist. ```bash veadk harness show --path my-harness @@ -113,22 +115,16 @@ veadk harness show --path my-harness It prints two sections: -1. **Configured agent params** — the `harness.yaml` contents: `harness_name`, - `model`, `tools`, `skills`, `system_prompt`, `runtime`, and any configured - `knowledgebase` / `long_term_memory` / `short_term_memory` components. -2. **Overridable at invoke time** — each `--` derived from - `HarnessOverrides` (`--model-name`, `--tools`, `--skills`, `--system-prompt`, - `--runtime`) with its description. These can be overridden per call via - `veadk harness invoke ... --`; memory and the knowledge base are **not** - overridable. +1. **Configured agent params** — the contents of `harness.yaml` (`harness_name`, `model`, `tools`, `skills`, `system_prompt`, `runtime`, and any configured `knowledgebase` / `long_term_memory` / `short_term_memory`). +2. **Overridable at invoke time** — each `--` derived from `HarnessOverrides` (`--model-name`, `--tools`, `--skills`, `--system-prompt`, `--runtime`) and its description. Memory and the knowledge base are **not** overridable. | Option | Description | | :-- | :-- | -| `--path` | Harness directory containing `harness.yaml`, default `.`. | +| `--path` | Directory containing `harness.yaml`, default `.`. | -### `veadk harness deploy` +## `veadk harness deploy` -Fill in deploy credentials, then from inside the directory: +With deploy credentials in place, run inside the directory: ```bash cd my-harness @@ -140,104 +136,58 @@ veadk harness deploy | :-- | :-- | :-- | | `--volcengine-access-key` | No | Volcengine access key (defaults to `VOLCENGINE_ACCESS_KEY`). | | `--volcengine-secret-key` | No | Volcengine secret key (defaults to `VOLCENGINE_SECRET_KEY`). | -| `--region` | No | AgentKit region (default `cn-beijing` or `VOLCENGINE_REGION`). | +| `--region` | No | AgentKit region (defaults to `cn-beijing` or `VOLCENGINE_REGION`). | | `--path` | No | Harness directory, default `.`. | -`deploy` loads `harness.yaml`, flattens it into the runtime's environment, and -runs an AgentKit **cloud** build (no local Docker) plus runtime create. The -runtime is named after `harness_name` (default `default`). On success it prints -the runtime name and its service endpoint; you can also find the endpoint in the -AgentKit console, then pass it to `veadk agentkit harness invoke --url ...`. +`deploy` loads `harness.yaml`, flattens it into the runtime environment, runs an AgentKit **cloud** build (no local Docker), and creates the runtime. The runtime is named after `harness_name`. -## Client command reference - -Once the server is deployed, `veadk agentkit harness` is the HTTP client used to -register and invoke harnesses against it at runtime. - -| Command | Description | -| :-- | :-- | -| `veadk agentkit harness add` | Register a new harness on the server. | -| `veadk agentkit harness invoke` | Invoke a harness and print its output. | +On success the endpoint and gateway API key are recorded into **`harness.json`** in the directory (shape `{name: {url, key, runtime_id}}`), so you can invoke it with `veadk harness invoke --name ` without copying the URL / key by hand: -## Connection options +```text +Harness runtime deployed: name=research-agent +Runtime id: r-xxxx +Endpoint: https://xxxx.apigateway-cn-beijing.volceapi.com +API key: **** -Both commands need the server address and auth, supplied via options or -environment variables: - -| Option | Env var | Description | -| :-- | :-- | :-- | -| `--url` | `HARNESS_URL` | Harness server base URL (required). | -| `--key` | `HARNESS_KEY` | Gateway API key for Bearer auth (optional). | -| — | `HARNESS_TIMEOUT` | Client request timeout in seconds, default `600` (agent runs can be slow). | - -## `harness add` - -Register a named harness on the server. - -```bash -veadk agentkit harness add \ - --name research-agent \ - --model-name doubao-seed-1-6-250615 \ - --system-prompt "You are a research assistant." \ - --tools web_search,web_fetch \ - --url "" --key "" +Recorded in .../harness.json. Invoke it with: + veadk harness invoke --name research-agent --message "" ``` -| Option | Required | Description | -| :-- | :-- | :-- | -| `--name` | Yes | Harness (agent) name. | -| `--model-name` | No | Model name; defaults to the server's `MODEL_NAME`. | -| `--system-prompt` | No | System prompt; defaults to `You are a helpful assistant.`. | -| `--tools` | No | Comma-separated built-in tool names, e.g. `web_search,web_fetch`. | -| `--skills` | No | Comma-separated skill hub names, e.g. `clawhub/lgwventrue/system-file-handler`. | -| `--runtime` | No | Agent runtime backend, `adk` (default) or `codex`. `codex` requires the optional codex extra on the server. | - - - If a harness with the same name already exists, the server returns `code: 400` - and does not overwrite it. - - -## `harness invoke` +## `veadk harness invoke` -Invoke an **already-registered** harness and print its output. The message is a -positional argument. +Invoke a **deployed** harness and print its output. The `url` and `key` are resolved from `harness.json` (written by `deploy`) by `--name`, so you need not pass them explicitly. ```bash -veadk agentkit harness invoke \ - --harness research-agent \ - --url "" --key "" \ - "Summarize the latest on reinforcement learning." +veadk harness invoke --name research-agent --message "Summarize recent progress in RL." ``` +The message may be given via `--message` / `-m` or as a positional argument. + | Option | Required | Description | | :-- | :-- | :-- | -| `MESSAGE` (positional) | Yes | The message to send to the harness. | -| `--harness` | Yes | Harness name to invoke. | +| `--name` / `--harness` | Yes | Harness name; its `url`/`key` are read from `harness.json`. | +| `MESSAGE` (positional) or `--message` / `-m` | Yes | The message to send to the harness. | | `--user-id` | No | User id for the session, default `cli-user`. | -| `--session-id` | No | Session id for the call, default `cli-session`. | -| `--model-name` | No | Override the model for this call only (creates a one-time harness). | -| `--system-prompt` | No | Override the system prompt for this call only (creates a one-time harness). | -| `--tools` | No | Override tools for this call only, comma-separated (creates a one-time harness). | -| `--skills` | No | Override skills for this call only, comma-separated (creates a one-time harness). | -| `--runtime` | No | Override the runtime for this call only, `adk` or `codex` (creates a one-time harness). | +| `--session-id` | No | Session id for the call. | +| `--url` | No | Harness URL (default `harness.json[name]`, or `HARNESS_URL`). | +| `--key` | No | API key for Bearer auth (default `harness.json[name]`, or `HARNESS_KEY`). | +| `--path` | No | Directory containing `harness.json`, default `.`. | +| `--model-name` | No | Override the model for this call only. | +| `--tools` | No | Override tools for this call only (comma-separated). | +| `--skills` | No | Override skills for this call only (comma-separated). | +| `--system-prompt` | No | Override the system prompt for this call only. | +| `--runtime` | No | Override the runtime for this call only (`adk` / `codex`). | -### One-time harness override +### Once-time overrides -If any of `--model-name` / `--system-prompt` / `--tools` / `--skills` / `--runtime` -is passed to `invoke`, the server builds a **one-time harness** from those fields -that applies to this single call only — the stored harness of the same name is -left untouched: +If any of `--model-name` / `--tools` / `--skills` / `--system-prompt` / `--runtime` is given, the server clones the deployed agent and applies the overrides **for that single call only**: tools and skills are added **incrementally** (deduped against existing ones), while memory and the knowledge base are **never** overridable. ```bash -veadk agentkit harness invoke \ - --harness research-agent \ - --system-prompt "Answer in one sentence." \ - --url "" --key "" \ - "What is reinforcement learning?" +veadk harness invoke --name research-agent \ + --tools get_city_weather \ + --message "What's the weather in Beijing today?" ``` ## Full example -For the end-to-end deploy-and-invoke flow (including the `curl` equivalents), -see the [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit) -example. +For the end-to-end deploy-and-invoke flow (with `curl` equivalents) see the example [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit). diff --git a/docs/content/docs/cli/harness-cli.mdx b/docs/content/docs/cli/harness-cli.mdx index 7e0e20c2..590742f8 100644 --- a/docs/content/docs/cli/harness-cli.mdx +++ b/docs/content/docs/cli/harness-cli.mdx @@ -1,30 +1,29 @@ --- title: "Harness 命令" -description: "通过命令行注册并调用部署在 Harness server 上的 harness(智能体规格)。" +description: "脚手架生成、配置、部署并调用一个 VeADK Harness server。" --- -`veadk agentkit harness` 是一组轻量的 HTTP 客户端命令,用于操作已部署的 **Harness server**(`veadk.cloud.harness_app`)。 +`veadk harness` 是一组命令,用于**脚手架生成、配置、部署并调用**一个 VeADK **Harness server**(`veadk.cloud.harness_app`)。 -一个 *harness* 就是一份具名的智能体规格 —— **模型 + 系统提示词 + 工具 + 技能**。Harness server 暴露 `/harness/add` 与 `/harness/invoke` 两个接口,让你在运行时动态注册并调用 harness,无需重新部署。 +一个 *harness* 就是一份智能体规格 —— **模型 + 系统提示词 + 工具 + 技能**,外加创建时绑定的**知识库与长/短期记忆**。整个规格通过分层的 `harness.yaml` 描述;`deploy` 会把它展平成 runtime 的环境变量,由 Harness server 在启动时组装成一个智能体,并通过 `POST /harness/invoke` 对外服务。 - 使用前需先把 Harness server 部署到火山引擎 AgentKit(参见下方 [部署 Harness server](#部署-harness-server),或示例 [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit)),并拿到服务的 **Endpoint URL** 与网关 **API Key**。 + 部署需要火山引擎 AgentKit 凭证(`VOLCENGINE_ACCESS_KEY` / `VOLCENGINE_SECRET_KEY`)。端到端示例参见 [`14_harness_server_on_agentkit`](https://github.com/volcengine/veadk-python/tree/main/examples/14_harness_server_on_agentkit)。 -## 部署 Harness server - -顶层命令组 `veadk harness` 用于脚手架生成并部署 Harness server 本身(与下文的 `veadk agentkit harness` 客户端命令相互独立)。 +## 命令一览 | 命令 | 描述 | | :-- | :-- | | `veadk harness create ` | 生成一个可部署的 harness 目录。 | | `veadk harness add` | 将智能体参数写入 `harness.yaml`。 | -| `veadk harness show` | 展示已配置参数以及可在调用时覆盖的参数。 | -| `veadk harness deploy` | 云端构建镜像并创建一个 AgentKit runtime。 | +| `veadk harness show` | 展示已配置参数,以及可在调用时覆盖的参数。 | +| `veadk harness deploy` | 云端构建镜像并创建 AgentKit runtime(无需本地 Docker)。 | +| `veadk harness invoke` | 调用一个已部署的 harness 并打印输出。 | -智能体通过分层的 `harness.yaml` 进行配置。 +典型流程:**`create` → `add` → `deploy` → `invoke`**。 -### `veadk harness create` +## `veadk harness create` 生成一个部署目录: @@ -35,77 +34,95 @@ veadk harness create my-harness 该命令会写入: - `harness.yaml` —— 智能体配置模板(见下)。 -- `.env.example` —— 仅包含火山引擎部署凭证(`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`,以及可选的 `VOLCENGINE_REGION`)。模型/智能体配置位于 `harness.yaml`。 +- `.env.example` —— 仅含火山引擎部署凭证(`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`,以及可选的 `VOLCENGINE_REGION`)。模型/智能体配置都在 `harness.yaml`。 - `Dockerfile` —— 构建 harness 服务镜像。 - `README.md` —— 快速开始说明。 -#### `harness.yaml` +### `harness.yaml` -分层配置文件。`deploy` 会将其展平为 runtime 的环境变量:嵌套键以 `_` 连接并转为大写,列表转为逗号分隔字符串,空值会被跳过。 +分层配置文件,每个组件自包含。`deploy` 会将其转换为 runtime 的环境变量:顶层字段与 `model` 直接展平(`model.name` → `MODEL_NAME`,`tools` → `TOOLS` 等),列表转为逗号分隔字符串;每个组件的 `type` 选择后端,组件下的其余参数映射为该后端实际读取的 VeADK 环境变量(如 viking 的 `project` → `DATABASE_VIKING_PROJECT`)。空值会被跳过,VeADK 回退到自身默认值。 ```yaml -harness_name: "" # -> HARNESS_NAME(runtime 名 + 知识库/记忆索引名) +harness_name: "" # -> HARNESS_NAME(runtime 名 + 知识库/长期记忆索引名) model: - name: "" # -> MODEL_NAME + name: "" # -> MODEL_NAME(Ark 鉴权在部署后由 runtime 的 IAM 角色解析) tools: [] # -> TOOLS(逗号连接) skills: [] # -> SKILLS(逗号连接) -system_prompt: "" # -> SYSTEM_PROMPT +system_prompt: "" # -> SYSTEM_PROMPT(空 = VeADK 默认) runtime: adk # -> RUNTIME("adk" 或 "codex") -knowledge_base: - type: "" # -> KNOWLEDGE_BASE_TYPE +knowledgebase: + type: "" # -> KNOWLEDGEBASE_TYPE(空 = 不创建) long_term_memory: - type: "" # -> LONG_TERM_MEMORY_TYPE + type: "" # -> LONG_TERM_MEMORY_TYPE(空 = 不创建) short_term_memory: type: local # -> SHORT_TERM_MEMORY_TYPE ``` -在 AgentKit runtime 上,Ark 鉴权由 runtime 的 IAM 角色解析,因此此处无需配置模型 API Key。未来的参数(额外的记忆/知识库设置)可以在各分段下扩展。 +每个组件下,按你设置的 `type` 取消对应后端参数的注释即可(模板里已逐项标注了环境变量与命令行 flag)。在 AgentKit runtime 上 Ark 鉴权由 IAM 角色解析,因此模型无需配置 API Key。 -### `veadk harness add` +## `veadk harness add` -将智能体参数写入 `./harness.yaml`(或 `--path`)。标量选项为「设置」语义;`--tool` / `--skill` 可重复,向列表「追加」(去重)。当 `harness.yaml` 不存在时快速失败。 +将智能体参数写入 `./harness.yaml`(或 `--path` 指定的目录)。当 `harness.yaml` 不存在时快速失败。 ```bash cd my-harness veadk harness add \ - --harness-name research-agent \ + --name research-agent \ --model-name doubao-seed-1-6-250615 \ --system-prompt "You are a research assistant." \ - --tool web_search --tool web_fetch \ + --tools web_search,web_fetch \ --runtime adk ``` | 选项 | 说明 | | :-- | :-- | -| `--harness-name` | 逻辑 harness / runtime 名(设置 `harness_name`)。 | +| `--name` / `--harness-name` | 逻辑 harness / runtime 名(设置 `harness_name`)。 | | `--model-name` | 推理模型名(设置 `model.name`)。 | -| `--tool` | 追加到 `tools` 的内置工具名(可重复)。 | -| `--skill` | 追加到 `skills` 的技能名(可重复)。 | +| `--tools` | 逗号分隔的内置工具名,如 `web_search,web_fetch`(设置 `tools`)。 | +| `--skills` | 逗号分隔的技能 hub 名(设置 `skills`)。 | | `--system-prompt` | 系统提示词 / 指令。 | -| `--runtime` | 智能体 runtime,`adk` 或 `codex`。 | -| `--knowledge-base-type` | 知识库后端(设置 `knowledge_base.type`)。 | +| `--runtime` | 智能体 runtime,`adk`(默认)或 `codex`。 | +| `--knowledgebase-type` | 知识库后端(设置 `knowledgebase.type`)。 | | `--long-term-memory-type` | 长期记忆后端(设置 `long_term_memory.type`)。 | | `--short-term-memory-type` | 短期记忆后端(设置 `short_term_memory.type`)。 | -| `--path` | `harness.yaml` 路径,默认 `./harness.yaml`。 | +| `--path` | 包含 `harness.yaml` 的目录,默认 `.`。 | -### `veadk harness show` +#### 组件连接参数 -打印 `harness.yaml` 中已配置的参数,以及可在每次调用时覆盖的参数。当 `harness.yaml` 不存在时快速失败。 +每个组件还会按其支持的后端,自动生成形如 `--<组件>-<参数>` 的连接参数 flag,例如: + +```bash +veadk harness add --name kb-agent \ + --knowledgebase-type viking \ + --knowledgebase-project my-project --knowledgebase-region cn-beijing +``` + +| 组件前缀 | 可用 flag(取决于后端) | +| :-- | :-- | +| `--knowledgebase-*` | `project` / `region`(viking)、`host` / `port` / `username` / `password` / `use-ssl` / `cert-path` / `secret-token`(opensearch)、`host` / `port` / `username` / `password` / `db`(redis) | +| `--long-term-memory-*` | 同上各后端,外加 `api-key` / `api-key-id` / `project-id` / `base-url`(mem0) | +| `--short-term-memory-*` | `host` / `user` / `password` / `database` / `charset` / `port`(mysql / postgresql) | + +凭证(access key / secret key)不通过这些 flag 传入,而是复用部署时 `.env` 里的 `VOLCENGINE_*`。 + +## `veadk harness show` + +打印 `harness.yaml` 中已配置的参数,以及可在每次调用时覆盖的参数。`harness.yaml` 不存在时快速失败。 ```bash veadk harness show --path my-harness ``` -该命令打印两个分段: +该命令打印两段: -1. **已配置的智能体参数** —— `harness.yaml` 的内容:`harness_name`、`model`、`tools`、`skills`、`system_prompt`、`runtime`,以及已配置的 `knowledgebase` / `long_term_memory` / `short_term_memory` 组件。 -2. **可在调用时覆盖的参数** —— 由 `HarnessOverrides` 派生的每个 `--`(`--model-name`、`--tools`、`--skills`、`--system-prompt`、`--runtime`)及其说明。这些参数可通过 `veadk harness invoke ... --` 按次覆盖;记忆与知识库**不可**覆盖。 +1. **已配置的智能体参数** —— `harness.yaml` 的内容(`harness_name`、`model`、`tools`、`skills`、`system_prompt`、`runtime`,以及已配置的 `knowledgebase` / `long_term_memory` / `short_term_memory`)。 +2. **可在调用时覆盖的参数** —— 由 `HarnessOverrides` 派生的每个 `--`(`--model-name`、`--tools`、`--skills`、`--system-prompt`、`--runtime`)及其说明。记忆与知识库**不可**覆盖。 | 选项 | 说明 | | :-- | :-- | -| `--path` | 包含 `harness.yaml` 的 harness 目录,默认 `.`。 | +| `--path` | 包含 `harness.yaml` 的目录,默认 `.`。 | -### `veadk harness deploy` +## `veadk harness deploy` 填好部署凭证后,在该目录内执行: @@ -117,91 +134,58 @@ veadk harness deploy | 选项 | 必填 | 说明 | | :-- | :-- | :-- | -| `--volcengine-access-key` | 否 | 火山引擎 Access Key(默认读取 `VOLCENGINE_ACCESS_KEY`)。 | -| `--volcengine-secret-key` | 否 | 火山引擎 Secret Key(默认读取 `VOLCENGINE_SECRET_KEY`)。 | +| `--volcengine-access-key` | 否 | 火山引擎 Access Key(默认读 `VOLCENGINE_ACCESS_KEY`)。 | +| `--volcengine-secret-key` | 否 | 火山引擎 Secret Key(默认读 `VOLCENGINE_SECRET_KEY`)。 | | `--region` | 否 | AgentKit 区域(默认 `cn-beijing` 或 `VOLCENGINE_REGION`)。 | | `--path` | 否 | harness 目录,默认 `.`。 | -`deploy` 会加载 `harness.yaml`,将其展平为 runtime 的环境变量,并执行 AgentKit 的**云端**构建(无需本地 Docker)以及 runtime 创建。runtime 以 `harness_name` 命名(默认 `default`)。成功后会打印 runtime 名及其服务 Endpoint;也可在 AgentKit 控制台查询 Endpoint,再传给 `veadk agentkit harness invoke --url ...`。 - -## 客户端命令一览 - -服务部署完成后,`veadk agentkit harness` 是用于在运行时注册并调用 harness 的 HTTP 客户端。 - -| 命令 | 描述 | -| :-- | :-- | -| `veadk agentkit harness add` | 在服务端注册一个新的 harness。 | -| `veadk agentkit harness invoke` | 调用一个 harness 并打印输出。 | - -## 连接参数 - -两个命令都需要指定 Harness server 的地址与鉴权,可通过命令行选项或环境变量提供: - -| 选项 | 环境变量 | 说明 | -| :-- | :-- | :-- | -| `--url` | `HARNESS_URL` | Harness server 的基础 URL(必填)。 | -| `--key` | `HARNESS_KEY` | 网关 API Key,用于 Bearer 鉴权(可选)。 | -| — | `HARNESS_TIMEOUT` | 客户端请求超时秒数,默认 `600`(智能体执行可能耗时较长)。 | +`deploy` 会加载 `harness.yaml`,将其展平为 runtime 环境变量,执行 AgentKit 的**云端**构建(无需本地 Docker)并创建 runtime。runtime 以 `harness_name` 命名。 -## `harness add` +成功后,端点与网关 API Key 会被记录到目录下的 **`harness.json`**(结构为 `{name: {url, key, runtime_id}}`),随后即可用 `veadk harness invoke --name ` 直接调用,无需手动复制 URL / Key: -在服务端注册一个具名 harness。 +```text +Harness runtime deployed: name=research-agent +Runtime id: r-xxxx +Endpoint: https://xxxx.apigateway-cn-beijing.volceapi.com +API key: **** -```bash -veadk agentkit harness add \ - --name research-agent \ - --model-name doubao-seed-1-6-250615 \ - --system-prompt "You are a research assistant." \ - --tools web_search,web_fetch \ - --url "" --key "" +Recorded in .../harness.json. Invoke it with: + veadk harness invoke --name research-agent --message "" ``` -| 选项 | 必填 | 说明 | -| :-- | :-- | :-- | -| `--name` | 是 | harness(智能体)名称。 | -| `--model-name` | 否 | 模型名称,缺省使用服务端的 `MODEL_NAME`。 | -| `--system-prompt` | 否 | 系统提示词,默认 `You are a helpful assistant.`。 | -| `--tools` | 否 | 逗号分隔的内置工具名,如 `web_search,web_fetch`。 | -| `--skills` | 否 | 逗号分隔的技能 hub 名,如 `clawhub/lgwventrue/system-file-handler`。 | -| `--runtime` | 否 | 智能体运行时,`adk`(默认)或 `codex`。`codex` 需服务端安装对应可选依赖。 | - - - 若同名 harness 已存在,服务端返回 `code: 400`,不会覆盖。 - - -## `harness invoke` +## `veadk harness invoke` -调用一个**已注册**的 harness,并打印其输出。消息作为位置参数传入。 +调用一个**已部署**的 harness 并打印输出。`url` 与 `key` 默认按 `--name` 从 `harness.json`(由 `deploy` 写入)解析,无需显式传入。 ```bash -veadk agentkit harness invoke \ - --harness research-agent \ - --url "" --key "" \ - "总结一下强化学习的最新进展。" +veadk harness invoke --name research-agent --message "总结一下强化学习的最新进展。" ``` +消息既可用 `--message` / `-m`,也可作为位置参数传入。 + | 选项 | 必填 | 说明 | | :-- | :-- | :-- | -| `MESSAGE`(位置参数) | 是 | 发送给 harness 的消息。 | -| `--harness` | 是 | 要调用的 harness 名称。 | +| `--name` / `--harness` | 是 | harness 名称;其 `url`/`key` 默认从 `harness.json` 读取。 | +| `MESSAGE`(位置参数)或 `--message` / `-m` | 是 | 发送给 harness 的消息。 | | `--user-id` | 否 | 会话所属用户 id,默认 `cli-user`。 | -| `--session-id` | 否 | 本次调用的会话 id,默认 `cli-session`。 | -| `--model-name` | 否 | 仅本次调用覆盖模型(生成一次性 harness)。 | -| `--system-prompt` | 否 | 仅本次调用覆盖系统提示词(生成一次性 harness)。 | -| `--tools` | 否 | 仅本次调用覆盖工具,逗号分隔(生成一次性 harness)。 | -| `--skills` | 否 | 仅本次调用覆盖技能,逗号分隔(生成一次性 harness)。 | -| `--runtime` | 否 | 仅本次调用覆盖运行时,`adk` 或 `codex`(生成一次性 harness)。 | +| `--session-id` | 否 | 本次调用的会话 id。 | +| `--url` | 否 | Harness URL(默认 `harness.json[name]`,或 `HARNESS_URL`)。 | +| `--key` | 否 | Bearer 鉴权 API Key(默认 `harness.json[name]`,或 `HARNESS_KEY`)。 | +| `--path` | 否 | 包含 `harness.json` 的目录,默认 `.`。 | +| `--model-name` | 否 | 仅本次调用覆盖模型。 | +| `--tools` | 否 | 仅本次调用覆盖工具(逗号分隔)。 | +| `--skills` | 否 | 仅本次调用覆盖技能(逗号分隔)。 | +| `--system-prompt` | 否 | 仅本次调用覆盖系统提示词。 | +| `--runtime` | 否 | 仅本次调用覆盖运行时(`adk` / `codex`)。 | -### 一次性 harness 覆盖 +### 一次性覆盖 -`invoke` 时若提供了 `--model-name` / `--system-prompt` / `--tools` / `--skills` / `--runtime` 中的任意一个,服务端会用这些字段构造一个**一次性 harness**,仅对本次调用生效,不影响已注册的同名 harness: +若提供了 `--model-name` / `--tools` / `--skills` / `--system-prompt` / `--runtime` 中的任意一个,服务端会克隆已部署的智能体并叠加这些覆盖,**仅对本次调用生效**:工具与技能为**增量**叠加(与已有同名项去重),记忆与知识库**永不**可覆盖。 ```bash -veadk agentkit harness invoke \ - --harness research-agent \ - --system-prompt "只用一句话回答。" \ - --url "" --key "" \ - "什么是强化学习?" +veadk harness invoke --name research-agent \ + --tools get_city_weather \ + --message "北京今天天气怎么样?" ``` ## 完整示例 From 7ee1f8d59e71219d9712c38b86d0049ecbec317e Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 20:36:57 +0800 Subject: [PATCH 12/15] test(harness): fix contract tests for redesigned harness_app API The contract tests still imported the removed `/harness/add` design (`AddHarnessRequest`/`AddHarnessResponse`/`Harness`/`_split_csv`), so the module failed to import. Rewrite against the current schemas: HarnessOverrides (per-call overridable) vs HarnessConfig (creation-time, with memory/kb), the InvokeHarness request/response models, and split_csv. --- tests/cloud/test_harness_app_contract.py | 129 +++++++++++------------ 1 file changed, 62 insertions(+), 67 deletions(-) diff --git a/tests/cloud/test_harness_app_contract.py b/tests/cloud/test_harness_app_contract.py index 8fbcdd74..c7cedd87 100644 --- a/tests/cloud/test_harness_app_contract.py +++ b/tests/cloud/test_harness_app_contract.py @@ -12,28 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Contract tests for the Harness server (``veadk.cloud.harness_app``). +"""Contract tests for the Harness server schemas (``veadk.cloud.harness_app``). -These pin the HTTP request/response *schemas* and the ``HarnessApp`` surface so -that a change to a model field name, default, or route silently breaking the -deployed server (or the ``veadk agentkit harness`` CLI client) is caught here -rather than in production. No network or model access is required. -""" +These pin the per-invocation override schema, the full creation-time config, and +the HTTP request/response models so that a change to a field name, default, or +the overridable/fixed split silently breaking the deployed server (or the +``veadk harness`` CLI, whose flags are generated from these fields) is caught +here rather than in production. -import inspect -from unittest import mock +Only ``types`` and ``utils`` are imported: ``app.py`` builds the live agent at +import time, so it is intentionally left out to keep these tests offline. +""" -from veadk.cloud import harness_app -from veadk.cloud.harness_app import ( - AddHarnessRequest, - AddHarnessResponse, - Harness, - HarnessApp, +from veadk.cloud.harness_app.types import ( + HarnessConfig, + HarnessOverrides, InvokeHarnessRequest, InvokeHarnessResponse, RunAgentRequest, ) +from veadk.cloud.harness_app.utils import split_csv from veadk.consts import DEFAULT_MODEL_AGENT_NAME +from veadk.prompts.agent_default_prompt import DEFAULT_INSTRUCTION def _fields(model) -> dict: @@ -41,9 +41,9 @@ def _fields(model) -> dict: return dict(model.model_fields) -class TestHarnessModel: +class TestHarnessOverrides: def test_fields(self): - assert set(_fields(Harness)) == { + assert set(_fields(HarnessOverrides)) == { "model_name", "tools", "skills", @@ -52,7 +52,7 @@ def test_fields(self): } def test_defaults(self): - fields = _fields(Harness) + fields = _fields(HarnessOverrides) assert fields["model_name"].default == DEFAULT_MODEL_AGENT_NAME assert fields["tools"].default == "" assert fields["skills"].default == "" @@ -60,22 +60,48 @@ def test_defaults(self): assert fields["runtime"].default == "adk" def test_tools_and_skills_are_csv_strings(self): - # The server splits these with _split_csv(); they must stay plain - # strings, not lists, to keep the CLI/curl pass-through contract. - h = Harness() + # The server splits these with split_csv(); they must stay plain strings, + # not lists, to keep the CLI/curl pass-through contract. + h = HarnessOverrides() assert isinstance(h.tools, str) assert isinstance(h.skills, str) + def test_every_field_has_a_description(self): + # Descriptions are the single source of truth for the generated + # `veadk harness invoke` flags, so each field must carry one. + for name, field in _fields(HarnessOverrides).items(): + assert field.description, f"{name} is missing a description" -class TestRequestResponseSchemas: - def test_add_request_fields(self): - assert set(_fields(AddHarnessRequest)) == {"harness_name", "harness"} - def test_add_response_fields_and_defaults(self): - fields = _fields(AddHarnessResponse) - assert set(fields) == {"code", "msg", "harness_name"} - assert fields["code"].default == 200 +class TestHarnessConfig: + def test_extends_overrides(self): + assert issubclass(HarnessConfig, HarnessOverrides) + + def test_adds_creation_time_fields(self): + assert set(_fields(HarnessConfig)) == set(_fields(HarnessOverrides)) | { + "app_name", + "knowledgebase_type", + "longterm_memory_type", + "shortterm_memory_type", + } + + def test_component_defaults(self): + fields = _fields(HarnessConfig) + # Empty backend = component disabled; short-term memory defaults to local. + assert fields["knowledgebase_type"].default == "" + assert fields["longterm_memory_type"].default == "" + assert fields["shortterm_memory_type"].default == "local" + + def test_system_prompt_default_is_veadk_instruction(self): + # HarnessConfig overrides the override-layer default with VeADK's own. + assert _fields(HarnessConfig)["system_prompt"].default == DEFAULT_INSTRUCTION + + def test_app_name_populated_via_name_alias(self): + assert HarnessConfig(name="research-agent").app_name == "research-agent" + assert HarnessConfig().app_name == "harness_app" + +class TestRequestResponseSchemas: def test_run_agent_request_fields(self): assert set(_fields(RunAgentRequest)) == {"user_id", "session_id"} @@ -87,10 +113,12 @@ def test_invoke_request_fields(self): "run_agent_request", } - def test_invoke_request_harness_is_optional(self): - # A null `harness` means "use the stored one"; a non-null one is the - # once-time override. The field must therefore allow None. - assert _fields(InvokeHarnessRequest)["harness"].default is None + def test_invoke_request_harness_is_optional_override(self): + # A null `harness` means "use the served agent"; a non-null one is the + # once-time override. The field must therefore allow None and default to it. + field = _fields(InvokeHarnessRequest)["harness"] + assert field.default is None + assert field.annotation == (HarnessOverrides | None) def test_invoke_response_fields_and_defaults(self): fields = _fields(InvokeHarnessResponse) @@ -98,45 +126,12 @@ def test_invoke_response_fields_and_defaults(self): assert fields["overwrite"].default is False -class TestHarnessApp: - def test_public_methods_exist(self): - for name in ("mount", "serve"): - assert callable(getattr(HarnessApp, name)) - - def test_serve_signature_defaults(self): - sig = inspect.signature(HarnessApp.serve) - assert sig.parameters["host"].default == "0.0.0.0" - assert sig.parameters["port"].default == 8000 - - def test_routes_registered(self): - app = HarnessApp() - paths = {getattr(route, "path", None) for route in app.app.routes} - assert {"/harness/add", "/harness/invoke"} <= paths - - def test_create_agent_passes_runtime(self): - # _create_agent must forward the harness runtime to the Agent. Mock Agent - # so no model client is built (keeps the test offline). - app = HarnessApp() - with mock.patch.object(harness_app, "Agent") as agent_cls: - app._create_agent(Harness(runtime="codex")) - assert agent_cls.call_args.kwargs["runtime"] == "codex" - - def test_create_agent_defaults_runtime_to_adk(self): - app = HarnessApp() - with mock.patch.object(harness_app, "Agent") as agent_cls: - app._create_agent(Harness()) - assert agent_cls.call_args.kwargs["runtime"] == "adk" - - class TestSplitCsv: def test_splits_and_trims(self): - assert harness_app._split_csv("web_search, web_fetch") == [ - "web_search", - "web_fetch", - ] + assert split_csv("web_search, web_fetch") == ["web_search", "web_fetch"] def test_empty_string_is_empty_list(self): - assert harness_app._split_csv("") == [] + assert split_csv("") == [] def test_drops_blank_segments(self): - assert harness_app._split_csv("a,, ,b") == ["a", "b"] + assert split_csv("a,, ,b") == ["a", "b"] From 0a8d1bfcb177a8ce43ec034e84a6f802b69c8d7f Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 20:42:11 +0800 Subject: [PATCH 13/15] refactor(harness): remove dead 'veadk agentkit harness add' command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server (harness_app) no longer exposes /harness/add — agents are assembled from env at deploy time, not registered at runtime — so the client 'add' command posted to a route that 404s. Remove it (and its contract tests); keep the working 'invoke' client and the shared _harness_request helper. --- .../cli/test_cli_agentkit_harness_contract.py | 47 +------------ veadk/cli/cli_agentkit.py | 68 +------------------ 2 files changed, 5 insertions(+), 110 deletions(-) diff --git a/tests/cli/test_cli_agentkit_harness_contract.py b/tests/cli/test_cli_agentkit_harness_contract.py index c9fba73b..4e257df8 100644 --- a/tests/cli/test_cli_agentkit_harness_contract.py +++ b/tests/cli/test_cli_agentkit_harness_contract.py @@ -29,52 +29,9 @@ def _options(command: click.Command) -> dict[str | None, click.Parameter]: return {param.name: param for param in command.params} -def test_harness_is_a_group_with_add_and_invoke(): +def test_harness_is_a_group_with_invoke(): assert isinstance(harness, click.Group) - assert set(harness.commands) == {"add", "invoke"} - - -class TestHarnessAdd: - def setup_method(self): - self.cmd = harness.commands["add"] - self.opts = _options(self.cmd) - - def test_option_names(self): - assert set(self.opts) == { - "name", - "model_name", - "system_prompt", - "tools", - "skills", - "runtime", - "url", - "key", - } - - def test_required_flags(self): - assert self.opts["name"].required is True - assert self.opts["url"].required is True - # Optional ones must stay optional. - for name in ( - "model_name", - "system_prompt", - "tools", - "skills", - "runtime", - "key", - ): - assert self.opts[name].required is False - - def test_runtime_choices(self): - assert isinstance(self.opts["runtime"].type, click.Choice) - assert set(self.opts["runtime"].type.choices) == {"adk", "codex"} - - def test_default_system_prompt(self): - assert self.opts["system_prompt"].default == "You are a helpful assistant." - - def test_env_var_bindings(self): - assert self.opts["url"].envvar == "HARNESS_URL" - assert self.opts["key"].envvar == "HARNESS_KEY" + assert set(harness.commands) == {"invoke"} class TestHarnessInvoke: diff --git a/veadk/cli/cli_agentkit.py b/veadk/cli/cli_agentkit.py index 5b0229ae..fbcc1325 100644 --- a/veadk/cli/cli_agentkit.py +++ b/veadk/cli/cli_agentkit.py @@ -33,9 +33,9 @@ def agentkit(): # --- Harness server client ------------------------------------------------- -# A thin HTTP client for a deployed Harness server (veadk/cloud/harness_app.py), -# which exposes `/harness/add` and `/harness/invoke`. Lives under a dedicated -# `harness` subgroup so it does not shadow the external AgentKit `invoke`. +# A thin HTTP client for a deployed Harness server (veadk/cloud/harness_app), +# which exposes `/harness/invoke`. Lives under a dedicated `harness` subgroup so +# it does not shadow the external AgentKit `invoke`. def _harness_request(url: str, path: str, key: str | None, body: dict) -> dict: @@ -67,68 +67,6 @@ def harness() -> None: pass -@harness.command("add") -@click.option("--name", required=True, help="Harness (agent) name.") -@click.option( - "--model-name", - "model_name", - default=None, - help="Model name for the harness (defaults to the server's MODEL_NAME).", -) -@click.option( - "--system-prompt", - "system_prompt", - default="You are a helpful assistant.", - help="System prompt for the harness.", -) -@click.option( - "--tools", - default=None, - help="Comma-separated built-in tool names, e.g. web_search,web_fetch.", -) -@click.option( - "--skills", - default=None, - help="Comma-separated skill hub names, e.g. clawhub/lgwventrue/system-file-handler.", -) -@click.option( - "--runtime", - default=None, - type=click.Choice(["adk", "codex"]), - help="Agent runtime backend (defaults to the server's 'adk').", -) -@click.option( - "--url", - required=True, - envvar="HARNESS_URL", - help="Harness server base URL (or set HARNESS_URL).", -) -@click.option( - "--key", - default=None, - envvar="HARNESS_KEY", - help="Gateway API key for Bearer auth (or set HARNESS_KEY).", -) -def harness_add( - name, model_name, system_prompt, tools, skills, runtime, url, key -) -> None: - """Register a new harness on the server.""" - spec: dict = {"system_prompt": system_prompt} - # Pass the comma-separated strings through; the server splits them. - if tools: - spec["tools"] = tools - if skills: - spec["skills"] = skills - if model_name: - spec["model_name"] = model_name - if runtime: - spec["runtime"] = runtime - result = _harness_request( - url, "/harness/add", key, {"harness_name": name, "harness": spec} - ) - click.echo(json.dumps(result, ensure_ascii=False)) - - @harness.command("invoke") @click.argument("message") @click.option( From efaa84be70420de795ac21332e097a0549a16620 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 11 Jun 2026 20:49:24 +0800 Subject: [PATCH 14/15] docs(examples): rewrite #14 harness example for the veadk harness flow The example documented the removed /harness/add registration, the old `agentkit config --entry_point ...` deploy flow, and `python -m veadk.cloud.harness_app` (now a package, no __main__). Rewrite both READMEs around create -> add -> deploy -> invoke with harness.json, and drop the orphaned .env.example / requirements.txt (the scaffold is now generated by `veadk harness create`). --- .../.env.example | 14 -- .../14_harness_server_on_agentkit/README.md | 157 ++++++++---------- .../README.zh.md | 147 +++++++--------- .../requirements.txt | 3 - 4 files changed, 129 insertions(+), 192 deletions(-) delete mode 100644 examples/14_harness_server_on_agentkit/.env.example delete mode 100644 examples/14_harness_server_on_agentkit/requirements.txt diff --git a/examples/14_harness_server_on_agentkit/.env.example b/examples/14_harness_server_on_agentkit/.env.example deleted file mode 100644 index 30bb05ef..00000000 --- a/examples/14_harness_server_on_agentkit/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -# Copy this file to `.env` and fill in your keys. -# VeADK auto-loads `.env` from the current working directory. - -# --- Agent reasoning model (Volcengine Ark) --- -# Get an API key at https://console.volcengine.com/ark -MODEL_AGENT_PROVIDER=openai -MODEL_AGENT_NAME=doubao-seed-1-6-250615 -MODEL_AGENT_API_BASE=https://ark.cn-beijing.volces.com/api/v3/ -MODEL_AGENT_API_KEY=your-ark-api-key-here - -# --- Volcengine AK/SK (required by `veadk agentkit` to build & deploy) --- -# Create access keys at https://console.volcengine.com/iam/keymanage -VOLCENGINE_ACCESS_KEY=your-volcengine-access-key -VOLCENGINE_SECRET_KEY=your-volcengine-secret-key diff --git a/examples/14_harness_server_on_agentkit/README.md b/examples/14_harness_server_on_agentkit/README.md index 6953304e..cf7c1c5a 100644 --- a/examples/14_harness_server_on_agentkit/README.md +++ b/examples/14_harness_server_on_agentkit/README.md @@ -1,136 +1,115 @@ # harness-server · Deploy the Harness server to Volcengine AgentKit Deploy VeADK's **Harness server** (`veadk.cloud.harness_app`) to -[Volcengine AgentKit](https://www.volcengine.com/) and call it over HTTP. +[Volcengine AgentKit](https://www.volcengine.com/) and call it over HTTP, using +the `veadk harness` CLI — no app code to write and no local Docker. > 中文版见 [README.zh.md](./README.zh.md) -A *harness* is a named agent spec — **model + system prompt + tools**. The -server lets you register harnesses at runtime (`/harness/add`) and invoke them -(`/harness/invoke`), with an optional once-time harness that overrides the -stored one for a single call. +A *harness* is an agent spec — **model + system prompt + tools + skills**, plus a +knowledge base and long/short-term memory bound at creation time. You describe it +in a layered `harness.yaml`; `deploy` flattens it into the runtime's environment +variables, and the server assembles the agent from them at startup and serves it +at `POST /harness/invoke`. -The server is shipped inside the `veadk` package, so there is **no app code to -write** — the runtime just runs the module: +The flow is **`create` → `add` → `deploy` → `invoke`**. See the full CLI +reference in the docs (`docs/content/docs/cli/harness-cli`). -```bash -python -m veadk.cloud.harness_app # serves the API on 0.0.0.0:8000 -``` +## 1. Scaffold -## What's in this directory - -```text -14_harness_server_on_agentkit/ -├── README.md # this file -├── .env.example # model + Volcengine credentials (placeholders) -└── requirements.txt # veadk-python (installed into the image) +```bash +veadk harness create harness-server +cd harness-server ``` -> `veadk agentkit config` / `launch` will generate `agentkit.yaml`, a -> `Dockerfile`, and a `.agentkit/` dir here — those are build artifacts and are -> gitignored, not part of the example. +This writes `harness.yaml` (the agent config), `.env.example` (Volcengine deploy +credentials only), a `Dockerfile`, and a `README.md`. -## API +## 2. Configure the agent -- `POST /harness/add` — body `{harness_name, harness}`. Register a harness; - returns `code: 400` if the name already exists. -- `POST /harness/invoke` — body - `{prompt, harness_name, harness?, run_agent_request}`. Run a **previously - added** harness. A non-null `harness` overrides the stored one for this call - (`overwrite: true`). +Set parameters into `harness.yaml` with `veadk harness add` (or edit the file): -`harness` fields: `model_name`, `system_prompt`, `tools`, `skills`, and -`runtime` (`"adk"` default, or `"codex"`). `tools` accepts either a list -(`["web_search", "web_fetch"]`) or a comma-separated string -(`"web_search,web_fetch"`). `run_agent_request` fields: `user_id`, `session_id`. +```bash +veadk harness add \ + --name research-agent \ + --model-name doubao-seed-1-6-250615 \ + --system-prompt "You are a research assistant." \ + --tools web_search,web_fetch \ + --runtime adk +``` Built-in tool names come from `veadk.tools.list_builtin_tools()` (e.g. `web_search`, `web_fetch`, `vesearch`, `link_reader`, `run_code`, `coding`, -`image_generate`, `image_edit`, `video_generate`, `text_to_speech`). - -## 1. Configure +`image_generate`, `image_edit`, `video_generate`, `text_to_speech`). On an +AgentKit runtime Ark auth is resolved by the runtime's IAM role, so the model +needs no API key — only its name. Review what's configured with: ```bash -cd examples/14_harness_server_on_agentkit -cp .env.example .env -# edit .env: MODEL_AGENT_API_KEY + VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY +veadk harness show ``` -The cloud function has no `.env`, so the model credentials are baked into the -runtime as env vars at config time: +## 3. Deploy ```bash -veadk agentkit config \ - --agent_name harness-server \ - --entry_point veadk.cloud.harness_app.py \ - --language Python --language_version 3.12 \ - --launch_type cloud --region cn-beijing \ - --dependencies_file requirements.txt \ - -e MODEL_AGENT_PROVIDER="$MODEL_AGENT_PROVIDER" \ - -e MODEL_AGENT_NAME="$MODEL_AGENT_NAME" \ - -e MODEL_AGENT_API_BASE="$MODEL_AGENT_API_BASE" \ - -e MODEL_AGENT_API_KEY="$MODEL_AGENT_API_KEY" +cp .env.example .env # then set VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY +veadk harness deploy ``` -`entry_point` is a dotted module path (the trailing `.py` is stripped), so -AgentKit runs the container with `python -m veadk.cloud.harness_app`. +`deploy` runs an AgentKit **cloud** build (no local Docker) and creates a runtime +named after `harness_name`. On success the endpoint and gateway API key are +recorded into **`harness.json`** (`{name: {url, key, runtime_id}}`), so the next +step needs no manual URL/key copying. > **Tip (CN build network):** if `pip`/`uv` is slow pulling dependencies, point -> the build at a domestic mirror, e.g. set a `PIP_INDEX_URL` / -> `UV_INDEX_URL=https://mirrors.volces.com/pypi/simple/` runtime/build env. - -## 2. Run locally (optional) +> the build at a domestic mirror, e.g. +> `UV_INDEX_URL=https://mirrors.volces.com/pypi/simple/`. -```bash -python -m veadk.cloud.harness_app # http://127.0.0.1:8000 -``` - -## 3. Deploy +## 4. Invoke ```bash -veadk agentkit launch # builds the image and deploys; prints the endpoint URL +veadk harness invoke --name research-agent \ + --message "Summarize the latest on reinforcement learning." ``` -## 4. Test +`url`/`key` are read from `harness.json` by `--name`; pass `--url` / `--key` +(or `HARNESS_URL` / `HARNESS_KEY`) to target a server explicitly. -Replace `` with the URL printed by `launch` and `` with the -runtime's gateway key (`veadk agentkit runtime get -r ` → -`AuthorizerConfiguration.KeyAuth.ApiKey`). +### Once-time overrides -With the VeADK CLI: +Passing any of `--model-name` / `--tools` / `--skills` / `--system-prompt` / +`--runtime` clones the deployed agent and applies the override **for that single +call only** (tools/skills are added incrementally; memory and the knowledge base +are never overridable): ```bash -veadk agentkit harness add \ - --name research-agent \ - --model-name doubao-seed-1-6-250615 \ - --system-prompt "You are a research assistant." \ - --tools web_search,web_fetch \ - --url "" --key "" - -veadk agentkit harness invoke \ - --harness research-agent \ - --url "" --key "" \ - "Summarize the latest on reinforcement learning." +veadk harness invoke --name research-agent \ + --tools get_city_weather \ + --message "What's the weather in Beijing today?" ``` -`--url` / `--key` can also be supplied via `HARNESS_URL` / `HARNESS_KEY`. +## API -Or with `curl` (gateway auth is `Authorization: Bearer `): +The server exposes a single endpoint: -```bash -curl -s -X POST "/harness/add" \ - -H "Authorization: Bearer " -H "Content-Type: application/json" \ - -d '{"harness_name":"bot","harness":{"system_prompt":"Be concise."}}' +- `POST /harness/invoke` — body + `{prompt, harness_name, harness?, run_agent_request}`. Runs the deployed agent; + a non-null `harness` is the once-time override for this call (response + `overwrite: true`). `harness` fields: `model_name`, `system_prompt`, `tools`, + `skills`, `runtime` (`tools`/`skills` are comma-separated strings). + `run_agent_request` fields: `user_id`, `session_id`. + +`curl` equivalent (gateway auth is `Authorization: Bearer `): +```bash curl -s -X POST "/harness/invoke" \ -H "Authorization: Bearer " -H "Content-Type: application/json" \ - -d '{"prompt":"Hello","harness_name":"bot","run_agent_request":{"user_id":"u1","session_id":"s1"}}' + -d '{"prompt":"Hello","harness_name":"research-agent","run_agent_request":{"user_id":"u1","session_id":"s1"}}' ``` ## Note on scaling -The harness registry is held **in memory per instance**. If the runtime scales -to multiple instances, an `add` on one instance is not visible to an `invoke` -routed to another. For a registered-then-invoked workflow, pin the runtime to a -single instance (`MinInstance = MaxInstance = 1`), or externalize the registry -(DB / cache) to share state across instances. +The short-term memory is held **in memory per instance**. If the runtime scales +to multiple instances, a session served by one instance is not visible to another. +To keep multi-turn sessions consistent, pin the runtime to a single instance +(`MinInstance = MaxInstance = 1`), or configure a shared `short_term_memory` +backend (e.g. `mysql` / `postgresql`) in `harness.yaml`. diff --git a/examples/14_harness_server_on_agentkit/README.zh.md b/examples/14_harness_server_on_agentkit/README.zh.md index 3ac8b5e2..c800118b 100644 --- a/examples/14_harness_server_on_agentkit/README.zh.md +++ b/examples/14_harness_server_on_agentkit/README.zh.md @@ -1,131 +1,106 @@ # harness-server · 部署 Harness 服务到火山方舟 AgentKit 把 VeADK 的 **Harness 服务**(`veadk.cloud.harness_app`)部署到 -[火山引擎 AgentKit](https://www.volcengine.com/),并通过 HTTP 调用。 +[火山引擎 AgentKit](https://www.volcengine.com/),并用 `veadk harness` 命令行通过 +HTTP 调用——无需编写应用代码,也无需本地 Docker。 > English version: [README.md](./README.md) -一个 *harness* 就是一份带名字的 agent 配置 —— **模型 + system prompt + 工具**。 -服务端支持运行时注册 harness(`/harness/add`)和调用(`/harness/invoke`), -调用时还可以临时传一个一次性 harness 覆盖已注册的那份。 +一个 *harness* 就是一份智能体规格——**模型 + 系统提示词 + 工具 + 技能**,外加创建时 +绑定的知识库与长/短期记忆。规格写在分层的 `harness.yaml` 里;`deploy` 会把它展平成 +runtime 的环境变量,服务端在启动时据此组装智能体,并通过 `POST /harness/invoke` 对外 +服务。 -服务代码已经在 `veadk` 包里,**不需要你写任何应用代码**,运行时直接跑模块即可: +流程为 **`create` → `add` → `deploy` → `invoke`**。完整命令参考见文档 +(`docs/content/docs/cli/harness-cli`)。 + +## 1. 生成脚手架 ```bash -python -m veadk.cloud.harness_app # 在 0.0.0.0:8000 提供 API +veadk harness create harness-server +cd harness-server ``` -## 本目录内容 - -```text -14_harness_server_on_agentkit/ -├── README.md # 英文说明 -├── README.zh.md # 本文件 -├── .env.example # 模型 + 火山凭证(占位符) -└── requirements.txt # veadk-python(装进镜像) -``` +该命令写入 `harness.yaml`(智能体配置)、`.env.example`(仅含火山引擎部署凭证)、 +`Dockerfile` 和 `README.md`。 -> `veadk agentkit config` / `launch` 会在此目录生成 `agentkit.yaml`、`Dockerfile` -> 和 `.agentkit/`,这些是构建产物,已被 gitignore,不属于示例本身。 +## 2. 配置智能体 -## API +用 `veadk harness add` 将参数写入 `harness.yaml`(也可直接编辑文件): -- `POST /harness/add` —— 请求体 `{harness_name, harness}`,注册一个 harness; - 同名已存在返回 `code: 400`。 -- `POST /harness/invoke` —— 请求体 - `{prompt, harness_name, harness?, run_agent_request}`,运行一个**已注册**的 - harness。请求里带非空 `harness` 则对本次调用临时覆盖(`overwrite: true`)。 - -`harness` 字段:`model_name`、`system_prompt`、`tools`、`skills`、`runtime` -(运行时,默认 `"adk"`,可传 `"codex"`)。`tools` 既接受数组 -(`["web_search", "web_fetch"]`)也接受逗号分隔字符串(`"web_search,web_fetch"`)。 -`run_agent_request` 字段:`user_id`、`session_id`。 +```bash +veadk harness add \ + --name research-agent \ + --model-name doubao-seed-1-6-250615 \ + --system-prompt "You are a research assistant." \ + --tools web_search,web_fetch \ + --runtime adk +``` -内置工具名见 `veadk.tools.list_builtin_tools()`(如 `web_search`、`web_fetch`、 +内置工具名来自 `veadk.tools.list_builtin_tools()`(如 `web_search`、`web_fetch`、 `vesearch`、`link_reader`、`run_code`、`coding`、`image_generate`、`image_edit`、 -`video_generate`、`text_to_speech`)。 - -## 1. 配置 +`video_generate`、`text_to_speech`)。在 AgentKit runtime 上 Ark 鉴权由 runtime 的 +IAM 角色解析,因此模型只需名字、无需 API Key。用以下命令查看已配置内容: ```bash -cd examples/14_harness_server_on_agentkit -cp .env.example .env -# 编辑 .env:填 MODEL_AGENT_API_KEY + VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY +veadk harness show ``` -云函数没有 `.env`,所以模型凭证需要在 config 时作为运行时环境变量打进 runtime: +## 3. 部署 ```bash -veadk agentkit config \ - --agent_name harness-server \ - --entry_point veadk.cloud.harness_app.py \ - --language Python --language_version 3.12 \ - --launch_type cloud --region cn-beijing \ - --dependencies_file requirements.txt \ - -e MODEL_AGENT_PROVIDER="$MODEL_AGENT_PROVIDER" \ - -e MODEL_AGENT_NAME="$MODEL_AGENT_NAME" \ - -e MODEL_AGENT_API_BASE="$MODEL_AGENT_API_BASE" \ - -e MODEL_AGENT_API_KEY="$MODEL_AGENT_API_KEY" +cp .env.example .env # 然后设置 VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY +veadk harness deploy ``` -`entry_point` 是一个「点号模块路径」(结尾的 `.py` 会被去掉),所以 AgentKit -最终用 `python -m veadk.cloud.harness_app` 启动容器 —— 模块从镜像里安装好的 -`veadk` 包解析,因此本目录**不需要任何 Python 文件**。 - -> **国内构建网络提示**:如果 `pip`/`uv` 拉依赖很慢,把构建指向国内镜像,例如设置 -> 运行时/构建环境变量 `UV_INDEX_URL=https://mirrors.volces.com/pypi/simple/`。 +`deploy` 执行 AgentKit 的**云端**构建(无需本地 Docker),并创建以 `harness_name` +命名的 runtime。成功后端点与网关 API Key 会记录到 **`harness.json`** +(`{name: {url, key, runtime_id}}`),下一步无需手动复制 URL / Key。 -## 2. 本地运行(可选) +> **提示(国内构建网络):** 若 `pip`/`uv` 拉依赖慢,把构建指向国内镜像,例如 +> `UV_INDEX_URL=https://mirrors.volces.com/pypi/simple/`。 -```bash -python -m veadk.cloud.harness_app # http://127.0.0.1:8000 -``` - -## 3. 部署 +## 4. 调用 ```bash -veadk agentkit launch # 构建镜像并部署,结束后打印访问 endpoint +veadk harness invoke --name research-agent \ + --message "总结一下强化学习的最新进展。" ``` -## 4. 测试 +`url`/`key` 默认按 `--name` 从 `harness.json` 读取;也可显式传 `--url` / `--key` +(或设 `HARNESS_URL` / `HARNESS_KEY`)指向某个服务。 -把 `` 换成 `launch` 打印的 URL,`` 换成 runtime 的网关 key -(`veadk agentkit runtime get -r ` → -`AuthorizerConfiguration.KeyAuth.ApiKey`)。 +### 一次性覆盖 -用 VeADK CLI: +提供 `--model-name` / `--tools` / `--skills` / `--system-prompt` / `--runtime` +中任意一个时,服务端会克隆已部署的智能体并叠加覆盖,**仅对本次调用生效**(工具/技能 +为增量叠加;记忆与知识库永不可覆盖): ```bash -veadk agentkit harness add \ - --name research-agent \ - --model-name doubao-seed-1-6-250615 \ - --system-prompt "你是一个研究助手。" \ - --tools web_search,web_fetch \ - --url "" --key "" - -veadk agentkit harness invoke \ - --harness research-agent \ - --url "" --key "" \ - "总结一下强化学习的最新进展。" +veadk harness invoke --name research-agent \ + --tools get_city_weather \ + --message "北京今天天气怎么样?" ``` -`--url` / `--key` 也可用环境变量 `HARNESS_URL` / `HARNESS_KEY` 代替。 +## API -或用 `curl`(网关鉴权为 `Authorization: Bearer `): +服务端只暴露一个接口: -```bash -curl -s -X POST "/harness/add" \ - -H "Authorization: Bearer " -H "Content-Type: application/json" \ - -d '{"harness_name":"bot","harness":{"system_prompt":"回答简洁。"}}' +- `POST /harness/invoke` —— 请求体 `{prompt, harness_name, harness?, run_agent_request}`。 + 运行已部署的智能体;非空的 `harness` 即本次调用的一次性覆盖(响应 `overwrite: true`)。 + `harness` 字段:`model_name`、`system_prompt`、`tools`、`skills`、`runtime` + (`tools`/`skills` 为逗号分隔字符串)。`run_agent_request` 字段:`user_id`、`session_id`。 + +`curl` 等价写法(网关鉴权为 `Authorization: Bearer `): +```bash curl -s -X POST "/harness/invoke" \ -H "Authorization: Bearer " -H "Content-Type: application/json" \ - -d '{"prompt":"你好","harness_name":"bot","run_agent_request":{"user_id":"u1","session_id":"s1"}}' + -d '{"prompt":"你好","harness_name":"research-agent","run_agent_request":{"user_id":"u1","session_id":"s1"}}' ``` ## 关于扩缩容 -harness 注册表是**按实例存在内存里**的。如果 runtime 扩到多个实例,在实例 A 上 -`add` 的 harness,对被路由到实例 B 的 `invoke` 是不可见的。对于「先注册再调用」的 -用法,请把 runtime 固定为单实例(`MinInstance = MaxInstance = 1`),或者把注册表 -外置(数据库 / 缓存)以在多实例间共享状态。 +短期记忆**按实例存在于内存中**。若 runtime 扩到多实例,某实例上的会话对另一个实例不可见。 +要保证多轮会话一致,可把 runtime 固定为单实例(`MinInstance = MaxInstance = 1`), +或在 `harness.yaml` 中配置共享的 `short_term_memory` 后端(如 `mysql` / `postgresql`)。 diff --git a/examples/14_harness_server_on_agentkit/requirements.txt b/examples/14_harness_server_on_agentkit/requirements.txt deleted file mode 100644 index 2785cf1a..00000000 --- a/examples/14_harness_server_on_agentkit/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Installed into the AgentKit image build. veadk-python ships the Harness -# server (veadk.cloud.harness_app) and pulls FastAPI/uvicorn transitively. -veadk-python From 9841eca158ce0df4fd53ca3b511d1ab435332761 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Fri, 12 Jun 2026 10:39:59 +0800 Subject: [PATCH 15/15] feat(harness): add OAuth2/JWT (custom_jwt) deploy option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An `auth` block in harness.yaml (or --discovery-url / --allowed-id on deploy) switches the runtime gateway to custom_jwt: it admits only JWTs issued by the user pool at `discovery_url` whose audience is in `allowed_ids`. Absent an `auth` block, deploy keeps the existing key_auth path unchanged. The auth block is excluded from the flattened runtime env (it configures the gateway authorizer, not the container). No secret is involved — the user pool / client / external IdP are set up in the Volcengine Identity console and the CLI only references discovery_url + allowed_ids. custom_jwt deploys record {url, runtime_id, auth_type, discovery_url, allowed_ids} in harness.json. Verified end-to-end against a real user pool: deploy -> runtime Ready with a custom_jwt authorizer carrying the given discovery_url + allowed_clients, no key_auth. Docs (zh/en) updated. --- docs/content/docs/cli/harness-cli.en.mdx | 26 ++++ docs/content/docs/cli/harness-cli.mdx | 17 +++ veadk/cli/cli_harness.py | 163 ++++++++++++++++++----- veadk/cloud/harness_app/env_mapping.py | 6 +- 4 files changed, 175 insertions(+), 37 deletions(-) diff --git a/docs/content/docs/cli/harness-cli.en.mdx b/docs/content/docs/cli/harness-cli.en.mdx index a22dcdc6..2c7a458d 100644 --- a/docs/content/docs/cli/harness-cli.en.mdx +++ b/docs/content/docs/cli/harness-cli.en.mdx @@ -138,6 +138,8 @@ veadk harness deploy | `--volcengine-secret-key` | No | Volcengine secret key (defaults to `VOLCENGINE_SECRET_KEY`). | | `--region` | No | AgentKit region (defaults to `cn-beijing` or `VOLCENGINE_REGION`). | | `--path` | No | Harness directory, default `.`. | +| `--discovery-url` | No | OIDC discovery URL; enables OAuth2/JWT auth (overrides `auth.discovery_url`). | +| `--allowed-id` | No | Comma-separated allowed client IDs for OAuth2/JWT auth (overrides `auth.allowed_ids`). | `deploy` loads `harness.yaml`, flattens it into the runtime environment, runs an AgentKit **cloud** build (no local Docker), and creates the runtime. The runtime is named after `harness_name`. @@ -153,6 +155,30 @@ Recorded in .../harness.json. Invoke it with: veadk harness invoke --name research-agent --message "" ``` +### OAuth2 / JWT auth deploy (optional) + +By default the gateway uses an API key (key_auth). To gate the runtime with +OAuth2/JWT backed by a **Volcengine Identity user pool**, add an `auth` block to +`harness.yaml` — **its presence switches to custom_jwt; without it, the default +key_auth is used**: + +```yaml +auth: + discovery_url: "https://userpool-.userpool.auth.id.cn-beijing.volces.com/.well-known/openid-configuration" + allowed_ids: [""] # a client created in the console; the gateway only admits tokens whose aud matches +``` + +You can also override it at deploy time with `--discovery-url` / `--allowed-id`. +The `auth` block is **not** written into the container environment — it only +configures the runtime's gateway authorizer. + +- The user pool, client, and external IdP (e.g. Feishu) are set up once in the + **Identity console**; the CLI only references `discovery_url` + `allowed_ids` + and **never touches a secret**. +- A custom_jwt deploy records `{url, runtime_id, auth_type, discovery_url, allowed_ids}` + in `harness.json` (no key). Calling such a runtime requires your own + `Authorization: Bearer ` header — the CLI does not mint it. + ## `veadk harness invoke` Invoke a **deployed** harness and print its output. The `url` and `key` are resolved from `harness.json` (written by `deploy`) by `--name`, so you need not pass them explicitly. diff --git a/docs/content/docs/cli/harness-cli.mdx b/docs/content/docs/cli/harness-cli.mdx index 590742f8..682eeaa4 100644 --- a/docs/content/docs/cli/harness-cli.mdx +++ b/docs/content/docs/cli/harness-cli.mdx @@ -138,6 +138,8 @@ veadk harness deploy | `--volcengine-secret-key` | 否 | 火山引擎 Secret Key(默认读 `VOLCENGINE_SECRET_KEY`)。 | | `--region` | 否 | AgentKit 区域(默认 `cn-beijing` 或 `VOLCENGINE_REGION`)。 | | `--path` | 否 | harness 目录,默认 `.`。 | +| `--discovery-url` | 否 | OIDC discovery URL,启用 OAuth2/JWT 鉴权(覆盖 `auth.discovery_url`)。 | +| `--allowed-id` | 否 | 逗号分隔的允许 client ID,用于 OAuth2/JWT 鉴权(覆盖 `auth.allowed_ids`)。 | `deploy` 会加载 `harness.yaml`,将其展平为 runtime 环境变量,执行 AgentKit 的**云端**构建(无需本地 Docker)并创建 runtime。runtime 以 `harness_name` 命名。 @@ -153,6 +155,21 @@ Recorded in .../harness.json. Invoke it with: veadk harness invoke --name research-agent --message "" ``` +### OAuth2 / JWT 鉴权部署(可选) + +默认用网关 API Key(key_auth)。若想用**火山引擎 Identity 用户池**做 OAuth2/JWT 门禁,在 `harness.yaml` 加一个 `auth` 段即可——**有 `auth` 段就走 custom_jwt,没有就走默认 key_auth**: + +```yaml +auth: + discovery_url: "https://userpool-<池子id>.userpool.auth.id.cn-beijing.volces.com/.well-known/openid-configuration" + allowed_ids: [""] # 控制台建的客户端 client_id;网关只放行 aud 命中的 token +``` + +也可用 `--discovery-url` / `--allowed-id` 在 deploy 时覆盖。`auth` 段**不会**写入容器环境变量,只用于配置 runtime 网关的 authorizer。 + +- 用户池、客户端、外部身份提供商(如飞书)在 **Identity 控制台**一次性建好,CLI 只**引用** `discovery_url` + `allowed_ids`,**不涉及任何 secret**。 +- custom_jwt 部署后 `harness.json` 记录 `{url, runtime_id, auth_type, discovery_url, allowed_ids}`(无 key)。调用该 runtime 需自带 `Authorization: Bearer <用户池签发的 JWT>`,CLI 不代为获取。 + ## `veadk harness invoke` 调用一个**已部署**的 harness 并打印输出。`url` 与 `key` 默认按 `--name` 从 `harness.json`(由 `deploy` 写入)解析,无需显式传入。 diff --git a/veadk/cli/cli_harness.py b/veadk/cli/cli_harness.py index d70b7f7b..8323a0cc 100644 --- a/veadk/cli/cli_harness.py +++ b/veadk/cli/cli_harness.py @@ -151,6 +151,17 @@ # user: postgres # password: "" # database: harness + +# --- Authentication (optional) ----------------------------------------------- +# Omit this block to deploy with the default API-key auth (key_auth). Add it to +# gate the runtime with OAuth2/JWT (custom_jwt): the API gateway then only accepts +# tokens issued by `discovery_url`'s user pool whose audience is one of +# `allowed_ids`. Set up the user pool / client / external IdP in the Volcengine +# Identity console; the CLI only references them (no secret involved). +# flags: --discovery-url / --allowed-id +# auth: +# discovery_url: "https://userpool-.userpool.auth.id.cn-beijing.volces.com/.well-known/openid-configuration" +# allowed_ids: [""] """ # `.env.example` carries ONLY deploy credentials. All model / agent config lives @@ -491,46 +502,54 @@ def show(path: str) -> None: def _build_agentkit_config( - runtime_name: str, region: str, envs: dict[str, str] + runtime_name: str, region: str, envs: dict[str, str], auth: dict | None = None ) -> dict: """Build the cloud AgentKit launch config dict (auto-provision). Mirrors the structure `agentkit init` produces for `launch_type: cloud`. The `{{account_id}}` / `{{timestamp}}` templates are resolved by AgentKit at deploy time and are passed through literally. + + When ``auth`` (a normalized ``{discovery_url, allowed_ids}`` block) is given, + the runtime is gated by OAuth2/JWT (``custom_jwt``); otherwise it keeps the + default API-key auth (``key_auth``). """ + cloud = { + "region": region, + "tos_bucket": "agentkit-platform-{{account_id}}", + "tos_prefix": "agentkit-builds", + "image_tag": "{{timestamp}}", + "cr_instance_name": "agentkit-platform-{{account_id}}", + "cr_namespace_name": "agentkit", + "cr_repo_name": runtime_name, + "cr_auto_create_instance_type": "Micro", + "build_timeout": 3600, + "cp_workspace_name": "agentkit-cli-workspace", + "cp_pipeline_name": "Auto", + "runtime_id": "Auto", + "runtime_name": runtime_name, + "runtime_role_name": "Auto", + } + if auth: + cloud["runtime_auth_type"] = "custom_jwt" + cloud["runtime_jwt_discovery_url"] = auth["discovery_url"] + cloud["runtime_jwt_allowed_clients"] = auth["allowed_ids"] + else: + cloud["runtime_auth_type"] = "key_auth" + cloud["runtime_apikey_name"] = "Auto" + cloud["runtime_apikey"] = "Auto" + cloud["runtime_jwt_allowed_clients"] = [] return { "common": { "agent_name": runtime_name, "entry_point": "app.py", - "description": "VeADK harness server", + "description": "Harness Server - VeADK", "language": "Python", "language_version": "3.12", "runtime_envs": envs, "launch_type": "cloud", }, - "launch_types": { - "cloud": { - "region": region, - "tos_bucket": "agentkit-platform-{{account_id}}", - "tos_prefix": "agentkit-builds", - "image_tag": "{{timestamp}}", - "cr_instance_name": "agentkit-platform-{{account_id}}", - "cr_namespace_name": "agentkit", - "cr_repo_name": runtime_name, - "cr_auto_create_instance_type": "Micro", - "build_timeout": 3600, - "cp_workspace_name": "agentkit-cli-workspace", - "cp_pipeline_name": "Auto", - "runtime_id": "Auto", - "runtime_name": runtime_name, - "runtime_role_name": "Auto", - "runtime_auth_type": "key_auth", - "runtime_apikey_name": "Auto", - "runtime_apikey": "Auto", - "runtime_jwt_allowed_clients": [], - } - }, + "launch_types": {"cloud": cloud}, "docker_build": {}, } @@ -546,16 +565,62 @@ def _load_harness_json(directory: str) -> dict: def _record_harness( - directory: str, name: str, url: str, key: str, runtime_id: str + directory: str, + name: str, + url: str, + runtime_id: str, + *, + key: str | None = None, + auth: dict | None = None, ) -> Path: - """Record/replace a deployed harness's url + key + id in `harness.json`.""" + """Record/replace a deployed harness in `harness.json`. + + key_auth records `{url, key, runtime_id}`; custom_jwt records + `{url, runtime_id, auth_type, discovery_url, allowed_ids}` (no key — a + user-pool JWT is supplied per request, not stored). + """ path = _harness_json_path(directory) data = _load_harness_json(directory) - data[name] = {"url": url, "key": key, "runtime_id": runtime_id} + if auth: + data[name] = { + "url": url, + "runtime_id": runtime_id, + "auth_type": "custom_jwt", + "discovery_url": auth["discovery_url"], + "allowed_ids": auth["allowed_ids"], + } + else: + data[name] = {"url": url, "key": key or "", "runtime_id": runtime_id} path.write_text(json.dumps(data, indent=2, ensure_ascii=False)) return path +def _resolve_auth( + yaml_auth: dict | None, discovery_url: str | None, allowed_id: str | None +) -> dict | None: + """Merge the `harness.yaml` `auth` block with deploy flag overrides. + + Returns a normalized ``{discovery_url, allowed_ids}`` to deploy with OAuth2/JWT + (custom_jwt), or ``None`` to keep the default API-key auth — the presence of an + `auth` block (or the flags) is the switch. Fails fast on a partial config. + """ + auth = dict(yaml_auth) if yaml_auth else {} + if discovery_url: + auth["discovery_url"] = discovery_url + if allowed_id: + auth["allowed_ids"] = [s.strip() for s in allowed_id.split(",") if s.strip()] + if not auth: + return None + discovery = auth.get("discovery_url") + allowed = auth.get("allowed_ids") or [] + if not discovery or not allowed: + raise click.ClickException( + "OAuth deploy needs both `auth.discovery_url` and `auth.allowed_ids` " + "(or --discovery-url and --allowed-id)." + ) + return {"discovery_url": discovery, "allowed_ids": list(allowed)} + + @harness.command("deploy") @click.option("--volcengine-access-key", default=None, help="Volcengine access key.") @click.option("--volcengine-secret-key", default=None, help="Volcengine secret key.") @@ -569,11 +634,23 @@ def _record_harness( default=".", help="Harness directory (created by `veadk harness create`).", ) +@click.option( + "--discovery-url", + default=None, + help="OIDC discovery URL; enables OAuth2/JWT auth (overrides `auth.discovery_url`).", +) +@click.option( + "--allowed-id", + default=None, + help="Comma-separated allowed client IDs for OAuth2/JWT auth (overrides `auth.allowed_ids`).", +) def deploy( volcengine_access_key: str | None, volcengine_secret_key: str | None, region: str | None, path: str, + discovery_url: str | None, + allowed_id: str | None, ) -> None: """Deploy the harness as an AgentKit runtime (cloud build, no local Docker). @@ -598,6 +675,7 @@ def deploy( data = _load_harness_yaml(proj_dir / "harness.yaml") runtime_envs = to_runtime_env(data) runtime_name = data.get("harness_name") or _DEFAULT_HARNESS_NAME + auth = _resolve_auth(data.get("auth"), discovery_url, allowed_id) # AgentKit authenticates via the Volcengine SDK, which reads VOLC_ACCESSKEY / # VOLC_SECRETKEY from the environment. Mirror whatever AK/SK was passed (or @@ -615,7 +693,7 @@ def deploy( ) resolved_region = region or os.getenv("VOLCENGINE_REGION") or "cn-beijing" - cfg = _build_agentkit_config(runtime_name, resolved_region, runtime_envs) + cfg = _build_agentkit_config(runtime_name, resolved_region, runtime_envs, auth) logger.info(f"Deploying harness runtime '{runtime_name}' from {proj_dir}") cwd = os.getcwd() @@ -645,17 +723,28 @@ def deploy( if runtime_id: lines.append(f"Runtime id: {runtime_id}") lines.append(f"Endpoint: {endpoint or '(see AgentKit console)'}") - if apikey: + if auth: + lines.append("Auth: custom_jwt (OAuth2/JWT gateway)") + lines.append(f"Discovery: {auth['discovery_url']}") + lines.append(f"Allowed ids: {', '.join(auth['allowed_ids'])}") + elif apikey: lines.append(f"API key: {apikey}") - if endpoint and apikey: + + if endpoint: json_path = _record_harness( - path, runtime_name, endpoint, apikey, runtime_id or "" + path, runtime_name, endpoint, runtime_id or "", key=apikey, auth=auth ) lines.append("") - lines.append(f"Recorded in {json_path}. Invoke it with:") - lines.append( - f' veadk harness invoke --name {runtime_name} --message ""' - ) + if auth: + lines.append( + f"Recorded in {json_path}. Auth is OAuth2/JWT — invoking requires an " + "`Authorization: Bearer ` header (the CLI does not mint it)." + ) + else: + lines.append(f"Recorded in {json_path}. Invoke it with:") + lines.append( + f' veadk harness invoke --name {runtime_name} --message ""' + ) click.secho( "\n".join(lines), fg="green", @@ -694,7 +783,9 @@ def deploy( help="API key for Bearer auth (default: harness.json[name], or HARNESS_KEY).", ) @click.option( - "--path", default=".", help="Dir containing harness.json (default: current dir)." + "--path", + default=".", + help="Dir containing harness.json (default: current dir).", ) @_override_options def invoke( diff --git a/veadk/cloud/harness_app/env_mapping.py b/veadk/cloud/harness_app/env_mapping.py index d76a2d69..95ea5b1e 100644 --- a/veadk/cloud/harness_app/env_mapping.py +++ b/veadk/cloud/harness_app/env_mapping.py @@ -159,7 +159,11 @@ def to_runtime_env(spec: dict[str, Any]) -> dict[str, str]: env: dict[str, str] = {} # Non-component fields: reuse VeADK's flatten_dict (same as config.yaml). - rest = {k: v for k, v in spec.items() if k not in COMPONENT_TYPE_ENV} + # The `auth` block is excluded too: it configures the runtime's gateway + # authorizer at deploy time (custom_jwt), not the container environment. + rest = { + k: v for k, v in spec.items() if k not in COMPONENT_TYPE_ENV and k != "auth" + } for key, value in flatten_dict(rest).items(): if _is_empty(value): continue