Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ LongHorizon-Harness turns existing agents into long-running computer-use systems

## ✨ News

- **[v0.1.7 · 2026-08-20]** A finished run is no longer a dead end: the workbench is now a conversation. Read the reply, type a follow-up, and the run continues on its own round ledger instead of replanning from scratch. A message you send mid-round is claimed by the very next round, so stopping and continuing never drops it. Also adds `--reasoning-effort` for every role (with `--manager-reasoning-effort` and friends to override one), forwarded to whichever backend exposes it. The transcript now reads in strict chronological order, and a graceful stop escalates to a force stop only when a worker ignores it.
- **[v0.1.6 · 2026-08-15]** Added [OpenCode](https://github.com/anomalyco/opencode) CLI support. LongHorizon-Harness can now run `opencode run prompt` as `--agent opencode`, with role-scoped read/write permissions, OpenCode API endpoint overrides, normalized JSON results, and CLI/config/doctor integration. The Web workbench can select OpenCode Harness and its model independently for each role.
- **[v0.1.5 · 2026-08-14]** Added phase-1 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) CLI support. LongHorizon-Harness can now run `dsh --profile headless` as `--agent deepseek_harness`, with an isolated `DSH_HOME`, role-scoped read/write permissions, DeepSeek API endpoint overrides, normalized JSONL results, and CLI/config/doctor integration. The Web workbench can select DeepSeek Harness and its model independently for each role. GUI computer-use and MCP support will follow in a later phase; see [the CLI setup](#5-or-run-a-task-from-the-command-line).
- **[v0.1.4 · 2026-08-11]** The new Dashboard has landed: a React/FastAPI workbench you can drive entirely from the browser. Start a task, choose a backend and model per role, answer approvals, send an instruction mid-run, and stop or restart a run. Launch it with `lh-harness web`; see [Run a task in the browser](#4-run-a-task-in-the-browser-recommended).
Expand Down Expand Up @@ -236,7 +237,7 @@ This creates `./.lh-harness/config.toml` without replacing an existing file; use
lh-harness web --workspace-root .
```

This opens the workbench at `http://127.0.0.1:8799/`. Everything happens there: start a task, pick a backend and model per role, answer approval requests, send an instruction mid-run, and stop or restart a run. `--workspace-root` sets the default working directory for tasks created there; the remaining options are listed under [Dashboard commands](#dashboard-commands).
This opens the workbench at `http://127.0.0.1:8799/`. Everything happens there: start a task, pick a backend and model per role, answer approval requests, send an instruction mid-run, stop or restart a run, and keep asking follow-up questions after it finishes — a follow-up continues the same run from the rounds it already completed. `--workspace-root` sets the default working directory for tasks created there; the remaining options are listed under [Dashboard commands](#dashboard-commands).

#### 5. Or run a task from the command line

Expand Down Expand Up @@ -327,6 +328,7 @@ Task text, run IDs, and API keys are deliberately **not** configurable here; the
|---|---|---|
| `agent` | `"codex"` | Backend for every role unless a role overrides it: `codex`, `claude_code`, `opencode`, or `deepseek_harness`. |
| `model` | `"gpt-5.6-sol"` | Model for every role unless a role overrides it. Must be a model the chosen backend exposes. |
| `reasoning_effort` | commented out | Reasoning depth for every role unless a role overrides it, forwarded to whichever backend exposes it. Unset keeps the provider's own setting. |
| `env` | `"local"` | Execution environment. Only `local` today. |
| `runs_root` | `"./.lh-harness/runs"` | Where run directories are created. Each run gets `<runs_root>/<run-id>/`. |
| `workspace` | commented out | Working directory the agents operate in. Defaults to the directory `lh-harness` was started from, so a task acts on your real project; set it to isolate the run somewhere else. |
Expand Down Expand Up @@ -354,13 +356,13 @@ Per-episode limits in seconds. One episode is a single role invocation, not the

##### `[run.roles.*]`

Each role can take its own `agent` and `model`, so you can pay for a strong model only where it matters: a capable Manager and Auditor with a cheaper Executor, for example. Every field is commented out by default, meaning "inherit".
Each role can take its own `agent`, `model`, and `reasoning_effort`, so you can pay for a strong model only where it matters: a capable Manager and Auditor with a cheaper Executor, for example. Every field is commented out by default, meaning "inherit".

Resolution walks the chain until it finds a value:

```
gui_executor → executor → [run].agent / [run].model
cli_auditor → auditor → [run].agent / [run].model
gui_executor → executor → [run].agent / [run].model / [run].reasoning_effort
cli_auditor → auditor → [run].agent / [run].model / [run].reasoning_effort
```

| Section | Falls back to | Covers |
Expand Down
10 changes: 6 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ LongHorizon-Harness 将现有 Agent 变成可长期运行的 computer-use 系统

## ✨ News

- **[v0.1.7 · 2026-08-20]** 任务跑完不再是终点,工作台变成了一场对话:看完回复直接追问,任务会沿用自己已完成的轮次继续跑,而不是从头重新规划。运行中发出的消息会被下一轮立即取用,先停止再继续也不会漏掉。同时新增 `--reasoning-effort` 统一设置各角色的推理强度(也可用 `--manager-reasoning-effort` 等单独覆盖),并转发给支持该能力的后端。对话现在严格按时间顺序展示,而强制中止只在 worker 忽略正常停止时才会出现。
- **[v0.1.6 · 2026-08-15]** 新增 [OpenCode](https://github.com/anomalyco/opencode) CLI 支持。LongHorizon-Harness 现在可以通过 `--agent opencode` 调用 `opencode run prompt`,并支持按角色划分的读写权限、OpenCode API 端点覆盖、标准化 JSON 结果,以及 CLI/config/doctor 集成。Web 工作台可以为每个角色单独选择 OpenCode Harness 及其模型。
- **[v0.1.5 · 2026-08-14]** 第一阶段已适配 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) CLI。LongHorizon-Harness 现在可以通过 `--agent deepseek_harness` 调用 `dsh --profile headless`,并提供隔离的 `DSH_HOME`、按角色划分的读写权限、DeepSeek API 端点覆盖、标准化 JSONL 结果以及 CLI/config/doctor 接入。Web 工作台支持为每个角色分别选择 DeepSeek Harness 及其模型;GUI computer-use 和 MCP 支持将在后续阶段补充。使用方式见 [CLI 配置说明](#5-也可以用命令行运行任务)。
- **[v0.1.4 · 2026-08-11]** 新版 Dashboard 已上线:基于 React/FastAPI 的工作台,全部操作都能在浏览器里完成——发起任务、为每个角色分别选择后端和模型、处理审批、运行中追加指令、停止或重启任务。用 `lh-harness web` 启动,见[在网页上运行任务](#4-在网页上运行任务推荐)。
Expand Down Expand Up @@ -237,7 +238,7 @@ lh-harness init
lh-harness web --workspace-root .
```

该命令在 `http://127.0.0.1:8799/` 打开工作台,所有操作都在这里完成:发起任务、为每个角色分别选择后端和模型、处理审批请求、在运行中追加指令、停止或重启任务。`--workspace-root` 指定在工作台中创建任务时的默认工作目录,其余参数见 [Dashboard 命令](#dashboard-命令)。
该命令在 `http://127.0.0.1:8799/` 打开工作台,所有操作都在这里完成:发起任务、为每个角色分别选择后端和模型、处理审批请求、在运行中追加指令、停止或重启任务,以及在任务结束后继续追问——追问会沿用它已完成的轮次继续同一个任务。`--workspace-root` 指定在工作台中创建任务时的默认工作目录,其余参数见 [Dashboard 命令](#dashboard-命令)。

#### 5. 也可以用命令行运行任务

Expand Down Expand Up @@ -328,6 +329,7 @@ lh-harness check-update
|---|---|---|
| `agent` | `"codex"` | 所有角色使用的后端(角色可单独覆盖):`codex`、`claude_code`、`opencode` 或 `deepseek_harness`。 |
| `model` | `"gpt-5.6-sol"` | 所有角色使用的模型(角色可单独覆盖)。必须是所选后端支持的模型。 |
| `reasoning_effort` | 默认注释 | 所有角色的推理强度(角色可单独覆盖),转发给支持该能力的后端。不设置则沿用服务方自身的默认值。 |
| `env` | `"local"` | 执行环境,目前只有 `local`。 |
| `runs_root` | `"./.lh-harness/runs"` | 运行目录的根路径,每次运行生成 `<runs_root>/<run-id>/`。 |
| `workspace` | 默认注释 | Agent 实际操作的工作目录。默认就是启动 `lh-harness` 的那个目录,任务直接作用于你的真实项目;需要隔离到别处时才设置。 |
Expand Down Expand Up @@ -355,13 +357,13 @@ lh-harness check-update

##### `[run.roles.*]`

每个角色都可以单独指定 `agent` 与 `model`,因此可以只在关键位置使用强模型:例如 Manager 与 Auditor 用强模型、Executor 用更便宜的。所有字段默认都是注释状态,表示「继承」。
每个角色都可以单独指定 `agent`、`model` 与 `reasoning_effort`,因此可以只在关键位置使用强模型:例如 Manager 与 Auditor 用强模型、Executor 用更便宜的。所有字段默认都是注释状态,表示「继承」。

取值时沿以下链路回退,直到找到值为止:

```
gui_executor → executor → [run].agent / [run].model
cli_auditor → auditor → [run].agent / [run].model
gui_executor → executor → [run].agent / [run].model / [run].reasoning_effort
cli_auditor → auditor → [run].agent / [run].model / [run].reasoning_effort
```

| 配置段 | 回退到 | 作用范围 |
Expand Down
44 changes: 34 additions & 10 deletions frontend/core/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { MAX_ROUNDS, type Snapshot } from './types';

const STOPPING_STATUSES = new Set(['stopping', 'aborting', 'stop_requested', 'abort_requested']);

export type CommandName =
| 'help'
| 'runs'
Expand Down Expand Up @@ -33,7 +35,7 @@ export const COMMAND_CATALOG: readonly CommandDefinition[] = [
{ name: 'approve', args: '<approval_id> <action>', description: 'Resolve an approval', capability: 'approvals', requiresRun: true },
{ name: 'stop', description: 'Request a graceful stop', capability: 'stop', requiresRun: true },
{ name: 'abort', description: 'Abort the active run', capability: 'abort', requiresRun: true },
{ name: 'resume', description: 'Resume a finished run', capability: 'resume', requiresRun: true },
{ name: 'resume', description: 'Continue a stopped run from its recorded rounds', capability: 'resume', requiresRun: true },
{ name: 'details', description: 'Open run details', requiresRun: true },
{ name: 'events', description: 'Open the event panel', requiresRun: true },
{ name: 'artifacts', description: 'Open the artifact panel', requiresRun: true },
Expand All @@ -53,10 +55,19 @@ export interface NewRunOptions {
workspace?: string;
maxRounds?: number;
promptLanguage?: 'en' | 'zh';
roles?: Partial<Record<'manager' | 'executor' | 'auditor', { agent?: string; model?: string }>>;
roles?: Partial<Record<'manager' | 'executor' | 'auditor', { agent?: string; model?: string; reasoning_effort?: string }>>;
error?: string;
}

/**
* Effort values reach the backends as inline TOML (Codex) or argv (the others),
* so the characters that could terminate a TOML string are rejected here as
* well as server-side. An allow-list of tiers would be wrong: the set differs
* per backend and per model, and operators must be able to pass a value a newer
* backend adds.
*/
const EFFORT_RE = /^[A-Za-z0-9._:-]{1,64}$/u;

export function normaliseMaxRounds(value: string | number | null | undefined, fallback = 25): number {
const safeFallback = Math.min(MAX_ROUNDS, Math.max(1, Number.isSafeInteger(fallback) ? fallback : 25));
const text = String(value ?? '').trim();
Expand Down Expand Up @@ -101,22 +112,32 @@ export function parseNewRunArgs(args: readonly string[]): NewRunOptions {
if (parseFlags) {
let handledValueFlag = false;
for (const flag of [
'--agent', '--model', '--workspace',
'--manager-agent', '--manager-model',
'--executor-agent', '--executor-model',
'--auditor-agent', '--auditor-model',
'--agent', '--model', '--workspace', '--effort',
'--manager-agent', '--manager-model', '--manager-effort',
'--executor-agent', '--executor-model', '--executor-effort',
'--auditor-agent', '--auditor-model', '--auditor-effort',
] as const) {
const parsed = valueFor(token, flag, args[index + 1]);
if (parsed.error) return { task: task.join(' ').trim(), error: parsed.error };
if (parsed.value !== undefined) {
if (flag === '--effort' || flag.endsWith('-effort')) {
if (!EFFORT_RE.test(parsed.value)) {
return { task: task.join(' ').trim(), error: `${flag} 只能包含字母、数字、'.'、'_'、':' 或 '-',且不超过 64 个字符` };
}
}
if (flag === '--agent') agent = parsed.value;
else if (flag === '--model') model = parsed.value;
else if (flag === '--workspace') workspace = parsed.value;
else {
const match = /^--(manager|executor|auditor)-(agent|model)$/u.exec(flag);
else if (flag === '--effort') {
// A global --effort applies to every role, matching --agent/--model.
for (const role of ['manager', 'executor', 'auditor'] as const) {
roles[role] = { ...(roles[role] || {}), reasoning_effort: parsed.value };
}
} else {
const match = /^--(manager|executor|auditor)-(agent|model|effort)$/u.exec(flag);
if (match) {
const role = match[1] as 'manager' | 'executor' | 'auditor';
const field = match[2] as 'agent' | 'model';
const field = match[2] === 'effort' ? 'reasoning_effort' : (match[2] as 'agent' | 'model');
roles[role] = { ...(roles[role] || {}), [field]: parsed.value };
}
}
Expand Down Expand Up @@ -194,7 +215,10 @@ export function availableCommands(
if (command.capability && capabilities[command.capability] === false) return false;
if (command.name === 'inject' && snapshot && !snapshot.controls.can_inject) return false;
if (command.name === 'stop' && snapshot && !snapshot.controls.can_abort) return false;
if (command.name === 'abort' && snapshot && !snapshot.controls.can_abort) return false;
// `/abort` stays typable while stopping: escalating an ignored SIGTERM is
// the only lifecycle transition left, and `can_abort` is false by then.
if (command.name === 'abort' && snapshot && !snapshot.controls.can_abort
&& !STOPPING_STATUSES.has(String(snapshot.run.status || '').trim().toLowerCase())) return false;
if (command.name === 'resume' && snapshot && !snapshot.controls.can_resume) return false;
return true;
});
Expand Down
Loading
Loading