@sema-agent/core
Version:
Stateless, task-oriented AI agent core
107 lines (106 loc) • 7.09 kB
TypeScript
/**
* design/96 §C (S2) — **Goal 模式**:LLM 自报完成 + core 校验续跑(CC goal 对标:目标 + 完成判定 + 续跑)。
* `runGoal` 是 `runRepairLoop`/`verifyCompleted` 的 SIBLING —— thin composition over `runner.runTask`,零
* Runner core 改动。详 `design/96-references/GOAL-IMPLEMENTER-SPEC.md`(v3+r2 异源证伪式对照收敛)。
*
* 🔴 **完成判定双闸(reward-hack 红线)**:① LLM 自报完成 = 调内置 `declare_done` 工具(或 `submit_output`,有
* outputSchema 时)= **机器信号**,非泛化 `completed`;② core `doneCheck`(机械/oracle 校验)。**AND**:仅二者
* 都满足才停 —— LLM 自报但 doneCheck 否决 → 续跑(把 reason 围栏喂回),这是防"模型说我完成了就算数"的核心。
*
* 🔴 **G1 脱钩(不可绕)**:`status:"achieved"` = 双闸过 → **停迭代 + SURFACE**,`accepted` 恒 false。runGoal
* **无任何 commit/accept 副作用**(纯 loop,同 runRepairLoop SAFE-tier 永不 `fixed`)。要 accept,caller 自走
* design/77 Gate-1 out-of-process oracle 隔离边界,不在 goal helper 开后门。
*/
import type { RunnerSelfSeat } from "../core/runner/contracts.js";
import type { CheckpointToken } from "../core/checkpoint-store.js";
import type { TaskResult, TaskSpec } from "../core/types.js";
/** Read-only snapshot a `doneCheck` sees after one iteration. */
export interface GoalTurnState {
/** This iteration's full {@link TaskResult}. */
readonly result: TaskResult;
/** 1-based iteration index. */
readonly iteration: number;
/** The session threaded through the whole goal (so a doneCheck can read the session/working tree). */
readonly sessionId: string;
/** Cumulative tokens (own + nested) across iterations — observe-only (the budget gate is in the engine). */
readonly cumulativeTokens: number;
}
export interface GoalVerdict {
/** core verification passed? (AND-ed with the LLM self-report — the double gate). */
readonly done: boolean;
/** When not done: the specific feedback fed back to the LLM (fenced as untrusted data). */
readonly reason: string;
}
/** The caller's HONEST declaration of how `doneCheck` verifies — surfaced on the result so a
* `self_report_only` "achieved" is never mistaken for a mechanically-verified one (G1, reward-hack honesty). */
export type GoalVerificationKind = "mechanical" | "self_report_only";
export interface GoalSpec {
/** The goal (injected as iteration 1's objective + GOAL_COMPLETION_GUIDANCE via the trusted goalMode flag). */
objective: string;
/**
* 🔴 The CORE completion gate (REQUIRED). The LLM self-report (gate 1) is necessary but NOT sufficient: the
* goal stops as `achieved` only when gate 1 AND `doneCheck.done`. Provide a PURE read-check (it may read the
* session/working tree but must have no side effects — it can be cancelled/timed out). A throw/reject
* fail-closes to `failed`/`goal.donecheck_error` (never treated as achieved). Core ships no default —
* supplying it forces the caller to confront the reward-hack red line.
*/
doneCheck: (state: GoalTurnState, signal: AbortSignal) => Promise<GoalVerdict>;
/** Honest declaration of `doneCheck`'s substance (§3.2). `self_report_only` ⇒ surfaced on the result. */
verificationKind: GoalVerificationKind;
/** Hard iteration cap (≥1) — exceeding without the double gate → `max_iterations` (NOT achieved). */
maxIterations: number;
/** Per-iteration base spec (model/tools/toolPolicy/principal/env/outputSchema/…). `objective` is overridden
* per iteration; `sessionId` is minted to thread the whole goal if absent; `signal` is set by the engine. */
taskSpec: Omit<TaskSpec, "objective">;
/** Cumulative token ceiling (own + nested); reaching it stops with `budget`. */
budgetTokens?: number;
/** Whole-goal wall-clock ceiling (ms); reaching it stops with `budget`. */
totalTimeoutMs?: number;
/** Cancels the whole goal (folded into each iteration's signal + the doneCheck). */
signal?: AbortSignal;
/**
* design/73 §1 — aggregation key of the `TaskOutcome` emitted at the goal's terminal state (only when
* `verificationKind:"mechanical"` — the red line). Default: `goal:<sha256(objective) first 16 hex>`,
* deterministic per objective. Set it when the "same task" spans differently-worded objectives.
*/
taskSignature?: string;
}
export type GoalStatus = "achieved" | "max_iterations" | "budget" | "blocked" | "suspended" | "needs_review" | "failed" | "aborted";
/**
* #111 — which ceiling ended a `status:"budget"` run. The status set stays CLOSED (a consumer
* switching on it keeps compiling); this is the terminating FACTOR beside it, because the two
* ceilings ask the caller for opposite corrections: `"tokens"` = the allowance is spent (narrow the
* objective / raise `budgetTokens`), `"walltime"` = `totalTimeoutMs` elapsed (give it more time).
*/
export type GoalBudgetCause = "tokens" | "walltime";
export interface GoalResult {
readonly status: GoalStatus;
/** Last iteration's result; absent on a preflight-terminate (a stop BEFORE iteration 1 ran). */
readonly result?: TaskResult;
readonly iterations: number;
/** The last `doneCheck` verdict (present once a doneCheck ran). */
readonly lastVerdict?: GoalVerdict;
/** The caller's declared verification kind (always present — replaces the ambiguous "verifiedBy"). */
readonly verificationKind: GoalVerificationKind;
/** 🔴 G1 explicit: runGoal NEVER accepts the artifact. Always false. `achieved` = "stopped + awaiting
* review"; the caller decides acceptance (via a Gate-1 boundary), never runGoal. */
readonly accepted: false;
readonly cumulativeTokens: number;
/** Carried up on suspended/needs_review so the caller can resume the underlying task. */
readonly checkpointToken?: CheckpointToken;
/** Set on `failed` (incl. `goal.donecheck_error`). */
readonly errorCode?: string;
/** #111 — present IFF `status === "budget"` (the KEY is absent otherwise, so presence alone reads
* as "stopped on a ceiling"). See {@link GoalBudgetCause}. */
readonly budgetCause?: GoalBudgetCause;
}
/** Reserved name of the goal-completion signal tool (injected by `runGoal` when there is no outputSchema). */
export declare const DECLARE_DONE_TOOL_NAME = "DeclareDone";
/**
* Run a goal to a double-gated completion (or a bound). Iterates `runner.runTask` on ONE session: each
* iteration the LLM works and signals completion (declare_done / submit_output); `doneCheck` then verifies.
* Only `declare_done` AND `doneCheck.done` → `achieved` (surfaced, NEVER auto-accepted, G1). A rejected
* doneCheck feeds its reason back (fenced) and continues. Bounded by maxIterations / budget / wall-clock /
* cancel; a durable pause (suspended/needs_review) is a hard boundary that surfaces the checkpointToken.
*/
export declare function runGoal(runner: RunnerSelfSeat, spec: GoalSpec): Promise<GoalResult>;