Harness spec 3.2.0 in the CLI: effort levels, reasoning round-trip, no absolute paths
ci / check (push) Waiting to run
ci / check (push) Waiting to run
Levels (models/schema.json levels): provider/effort.ts levelsOf/levelFields/ readLevel — `off` only with thinking_switch and sends enable_thinking false and no effort, `default` for a model that takes no efforts and sends nothing, a level not offered is read as the start level. The engine's effort is a level; /effort shows nothing for one level, a Thinking switch for [off, X], a list otherwise; the status bar follows. /v1/models' thinking_switch, efforts [] and capacity.parallel are read (lembas/webui.ts); session/capacity.ts counts parallel slots. Reasoning (context.md §8): an assistant message of the current turn carries reasoning_content (provider/common.ts turnReasoning, openai-chat.ts); step-boundary deliveries, nudges and the wrap-up are marked turn: false and do not start a turn. Paths (context.md §11): the <env> block names the project and, started below the root, that directory relative to it (assemble.ts envLines); instruction files and @ attachments are named from the root or as ~/…; file tools and bash resolve relative paths against the project root. The team rules (team/team.ts) and conformance runners for levels, reasoning, paths and team; the team runtime follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
36 files changed
+1168
-153
No files matched your search
+17
-18
@@ -12,10 +12,9 @@ import { boardSummary } from "./project/board.ts"
|
||||
import { unsafeProjectFile } from "./project/safe.ts"
|
||||
import { Bus, type Asker } from "./bus/index.ts"
|
||||
import { loadConfig, type Loaded } from "./config/load.ts"
|
||||
import type { Effort, Mode } from "./config/schema.ts"
|
||||
import type { Mode } from "./config/schema.ts"
|
||||
import { DEFAULT_RULES, toRules } from "./permission/evaluate.ts"
|
||||
import { hardlineRules } from "./permission/hardline.ts"
|
||||
import { defaultEffort } from "./provider/effort.ts"
|
||||
import { clientFor, familyOf, resolveModel } from "./provider/index.ts"
|
||||
import { assembleSystem, instructionFiles, knowledgeScope, modeText } from "./prompt/assemble.ts"
|
||||
import type { Changing } from "./session/prefix.ts"
|
||||
@@ -45,14 +44,15 @@ import type { Plan } from "./plan/plan.ts"
|
||||
import { LIBRARY_SERVER, mcpInstructions } from "./mcp/index.ts"
|
||||
import type { McpServer } from "./config/schema.ts"
|
||||
import { embedderFor } from "./library/embed.ts"
|
||||
import { effortsFor } from "./provider/effort.ts"
|
||||
import { isLevel, levelsFor, readLevel, type Level } from "./provider/effort.ts"
|
||||
|
||||
export interface AppOptions {
|
||||
cwd: string
|
||||
model?: string
|
||||
mode?: Mode
|
||||
/** undefined = the model's default; "off" = none */
|
||||
effort?: Effort | "off"
|
||||
/** The effort level asked for (an effort, "off" or "default"); undefined, or one the model does
|
||||
* not offer: its start level. */
|
||||
effort?: string
|
||||
asker: Asker
|
||||
store?: Store | false
|
||||
/** Nobody can answer an approval (headless, no --yes). Said in the system prompt up front. */
|
||||
@@ -215,13 +215,10 @@ export function createApp(o: AppOptions): App {
|
||||
// An untrusted project is read-only: plan mode, and its own config is ignored.
|
||||
let mode: Mode = o.mode ?? loaded.config.mode ?? "manual"
|
||||
if (trust === "readonly") mode = "plan"
|
||||
// `effort:` in config when this model takes it, else the model's own default.
|
||||
const startEffort = (m: ResolvedModel) => {
|
||||
const e = loaded.config.effort
|
||||
if (e === "off") return null
|
||||
return e && effortsFor(m).includes(e) ? e : defaultEffort(m)
|
||||
}
|
||||
const effort = o.effort === "off" ? null : (o.effort ?? startEffort(model))
|
||||
// `effort:` in config when this model offers that level, else the model's own start level
|
||||
// (harness models/schema.json `levels`).
|
||||
const startEffort = (m: ResolvedModel): Level => readLevel(m, loaded.config.effort)
|
||||
const effort: Level = o.effort !== undefined && isLevel(o.effort) && (levelsFor(model).levels as string[]).includes(o.effort) ? o.effort : startEffort(model)
|
||||
// One object for the session's lifetime, so a setting changed later reaches the tools.
|
||||
loaded.config.search ??= {}
|
||||
const bus = o.bus ?? new Bus()
|
||||
@@ -468,7 +465,9 @@ export function createApp(o: AppOptions): App {
|
||||
// its server can take it beside the session's and beside every subagent already running
|
||||
// (src/session/capacity.ts), before anything is made.
|
||||
const m = opts.model ? resolveModel(loaded, opts.model) : spec.model ? resolveModel(loaded, spec.model) : engine.model
|
||||
const refused = capacityRefusal(engine.model, m) || registry.running(jobsSessionId()).filter((j) => j.kind === "subagent" && j.subagent).map((j) => capacityBeside(j.subagent!.engine.model, m)).find(Boolean)
|
||||
const running = registry.running(jobsSessionId()).filter((j) => j.kind === "subagent" && j.subagent)
|
||||
const onIt = running.filter((j) => j.subagent!.engine.model.ref === m.ref).length
|
||||
const refused = capacityRefusal(engine.model, m, onIt) || running.map((j) => capacityBeside(j.subagent!.engine.model, m, onIt)).find(Boolean)
|
||||
if (refused) throw new ToolError(`${refused} Do this part yourself, or hand it to a model on another connection.`)
|
||||
// A checkout and branch of its own, when asked for (the task's worktree, the agent's isolation).
|
||||
let wt: Worktree | undefined
|
||||
@@ -545,7 +544,7 @@ export function createApp(o: AppOptions): App {
|
||||
{
|
||||
name: spec.name,
|
||||
instructions: wt
|
||||
? `${spec.instructions}\n\nYou are working in a separate checkout of the repository, at ${at!.root}, on the branch ${wt.branch} (made from the last commit — uncommitted work of the main agent is not here). Work only inside it. When you finish, your changes are committed on that branch for the main agent to merge: do not commit, merge or push yourself.`
|
||||
? `${spec.instructions}\n\nYou are working in a separate checkout of the repository — its root is your project root, so relative paths reach it — on the branch ${wt.branch} (made from the last commit — uncommitted work of the main agent is not here). Work only inside it. When you finish, your changes are committed on that branch for the main agent to merge: do not commit, merge or push yourself.`
|
||||
: spec.instructions,
|
||||
report: childTools.some((t) => t.name === "report"),
|
||||
skills: childTools.some((t) => t.name === "skill_view"),
|
||||
@@ -721,19 +720,19 @@ export function createApp(o: AppOptions): App {
|
||||
},
|
||||
live(key) {
|
||||
if (key === "model") return engine.model.ref
|
||||
if (key === "effort") return engine.effort ?? "off"
|
||||
if (key === "effort") return engine.effort
|
||||
if (key === "mode") return engine.mode
|
||||
return key.split(".").reduce<unknown>((x, k) => (x && typeof x === "object" ? (x as Record<string, unknown>)[k] : undefined), loaded.config)
|
||||
},
|
||||
check(key, v) {
|
||||
if (key === "model" || key === "small_model") resolveModel(loaded, String(v))
|
||||
if (key === "effort" && v !== "off" && !effortsFor(engine.model).includes(v as Effort))
|
||||
throw new Error(`${engine.model.ref} takes ${effortsFor(engine.model).join(", ")} or off`)
|
||||
if (key === "effort" && !(levelsFor(engine.model).levels as string[]).includes(String(v)))
|
||||
throw new Error(`${engine.model.ref} offers the levels ${levelsFor(engine.model).levels.join(", ")}`)
|
||||
if (key === "knowledge") for (const b of v as string[]) if (!library.base(b)) throw new Error(`there is no knowledge base "${b}" — lembas kb lists them`)
|
||||
},
|
||||
apply(key, v) {
|
||||
if (key === "model") switchModel(String(v))
|
||||
else if (key === "effort") engine.effort = v === "off" ? null : (v as Effort)
|
||||
else if (key === "effort") engine.effort = v as Level
|
||||
else if (key === "mode") engine.mode = v as Mode
|
||||
else if (key === "limits.steps") engine.o.maxSteps = Number(v)
|
||||
else if (key === "busy_input") engine.busyInput = v as "steer" | "queue"
|
||||
|
||||
Reference in new issue
Block a user