fix(goal): simplify blockers into one durable phase
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-persisted-same-session-goal-domain.md: 1484a919851c18ce978c93c068f6096bbf3b733f
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: 8bcedc6cab6b9219a33a655e8fcbc53762b7413f
|
||||
2026-07-19-persisted-same-session-goal-domain.md: 75355aa8d94789e0cc227d393c50139708a73f6a
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: fd7e31f4b6a5acec1d2b44fb3088e301b36abd5c
|
||||
@@ -12,13 +12,13 @@ Durable lifecycle and permission to continue are different facts. A session may
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-goal` in `packages/goal/goal/` owns one current same-session goal through `ctx.goals`. A goal has a branded id, objective, durable phase, compare-and-set revision, and `maxGoalRounds`. `defaultMaxGoalRounds` is a validated deployment setting with default `256`; `resolveCreate()` materializes it before mutation.
|
||||
`@deepseek-ai/dsh-goal` in `packages/goal/goal/` owns one current same-session goal through `ctx.goals`. A goal has a branded id, objective, durable phase, compare-and-set revision, and `maxGoalRounds`. `defaultMaxGoalRounds` is a validated deployment setting with default `256`; `create()` materializes it internally before mutation rather than exposing resolution as another service verb.
|
||||
|
||||
The durable phases are `active`, `paused`, `blocked`, `usage-limited`, `budget-limited`, and `complete`. A separate live activation is `armed` or `disarmed`. Creation and explicit resume arm activation; pause, completion, blocking, limit transitions, and clear disarm it. Edits preserve activation. Activation is never part of the persisted snapshot.
|
||||
The durable phases are `active`, `paused`, `blocked`, and `complete`. A blocked snapshot includes a policy-owned lower-kebab-case code and a normalized free-form message, so usage limits, round caps, execution failures, and human-input dependencies share one lifecycle state without losing their cause. A separate live activation is `armed` or `disarmed`. Creation and explicit resume arm activation; pause, completion, blocking, and clear disarm it. Edits preserve activation and any blocker reason; resume and completion clear that reason. Activation is never part of the persisted snapshot.
|
||||
|
||||
### Durable record and replay
|
||||
|
||||
Every non-clear mutation uses `Agent.inject()` to append a raw, model-visible `context/message` containing a versioned full snapshot. Clear appends a revisioned tombstone. The context source is `{ kind: 'goal', goalId, revision, round: 0 }`; metadata and rendered `<goal_state>...</goal_state>` content must agree exactly. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field.
|
||||
Every non-clear mutation uses `Agent.inject()` to append a raw, model-visible `context/message` containing a versioned full snapshot. Clear appends a revisioned tombstone. The context source is `{ kind: 'goal', goalId, revision, round: 0 }`; metadata and rendered `<goal_state>...</goal_state>` content must agree exactly. This descriptive delimiter follows the repository's existing `<workspace_context>` convention and [Anthropic's published guidance to structure mixed prompt content with consistent descriptive XML tags](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags). That is public model-experience prior art, not evidence about any provider's proprietary training corpus. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field.
|
||||
|
||||
The replay fold validates JSON shape, source attribution, rendered content, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds are positive sequential `user/message` source numbers for the current active revision and cannot exceed `maxGoalRounds`; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired.
|
||||
|
||||
@@ -26,7 +26,7 @@ When `Agent.inject()` defers a mutation inside an active tool batch, the service
|
||||
|
||||
### Lifecycle and live activation
|
||||
|
||||
At most one goal is current. Create requires no current non-complete goal and always generates a revision-one id not used earlier in the session; a completed goal may be replaced. Every other mutation carries the expected `GoalRef`, and stale ids or revisions reject. Resume accepts a stopped phase or a disarmed active goal only when the round cap has remaining capacity; budget limiting requires the admitted count to have reached the cap.
|
||||
At most one goal is current. Create requires no current non-complete goal and always generates a revision-one id not used earlier in the session; a completed goal may be replaced. Every other mutation carries the expected `GoalRef`, and stale ids or revisions reject. Resume accepts a paused or blocked phase, or a disarmed active goal, only when the round cap has remaining capacity. The domain validates blocker reason shape but deliberately leaves reason codes and the decision to block to policy consumers.
|
||||
|
||||
A cache built from any seed starts disarmed, and every `agent/session-start` edge disarms it again. Resume and fork therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy surface may explicitly call resume and arm the goal.
|
||||
|
||||
@@ -36,7 +36,7 @@ The service accepts only the exact live `Agent` object registered under its id.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, cap enforcement, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start disarming and active-goal rearming, FIFO deferred mutation reconciliation, reentrant append observation, rejected-injection rollback, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
|
||||
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, blocker reason validation and retention, cap enforcement on resume, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start disarming and active-goal rearming, FIFO deferred mutation reconciliation, reentrant append observation, rejected-injection rollback, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -52,7 +52,7 @@ Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set r
|
||||
- Resume and fork expose the same durable phase while remaining operationally inert until an explicit resume mutation arms activation.
|
||||
- Full snapshots simplify inspection and strict replay but repeat the objective and state fields in model history until compaction shadows them.
|
||||
- Revision and lifecycle validation reject tampered, partially written, or producer-inconsistent goal records early.
|
||||
- Round caps bound continuation count only; token, currency, time, and provider limits remain separate policy concerns.
|
||||
- Round caps bound continuation count only; policy consumers map round, token, currency, time, and provider limits to blocked reasons when they stop work.
|
||||
|
||||
## Known limitations and deferred work
|
||||
|
||||
|
||||
+6
-6
@@ -12,13 +12,13 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
位于 `packages/goal/goal/` 的 `@deepseek-ai/dsh-goal` 通过 `ctx.goals` 管理一个当前的同会话目标。目标包含品牌化 id、目标描述、持久阶段、比较并交换修订号和 `maxGoalRounds`。`defaultMaxGoalRounds` 是经过校验的部署配置,默认值为 `256`;`resolveCreate()` 在变更前将其解析为完整值。
|
||||
位于 `packages/goal/goal/` 的 `@deepseek-ai/dsh-goal` 通过 `ctx.goals` 管理一个当前的同会话目标。目标包含品牌化 id、目标描述、持久阶段、比较并交换修订号和 `maxGoalRounds`。`defaultMaxGoalRounds` 是经过校验的部署配置,默认值为 `256`;`create()` 在变更前于内部将其解析为完整值,而不会把解析过程暴露为额外的服务动词。
|
||||
|
||||
持久阶段包括 `active`、`paused`、`blocked`、`usage-limited`、`budget-limited` 和 `complete`。独立的实时激活态为 `armed` 或 `disarmed`。创建与显式恢复会激活目标;暂停、完成、阻塞、达到限制和清除都会解除激活。编辑保留激活态。持久快照绝不包含激活态。
|
||||
持久阶段包括 `active`、`paused`、`blocked` 和 `complete`。阻塞快照包含由策略提供的 kebab-case 小写代码和规范化自由文本消息,因此用量限制、回合上限、执行失败和等待人工输入可以共享一个生命周期状态而不丢失原因。独立的实时激活态为 `armed` 或 `disarmed`。创建与显式恢复会激活目标;暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因;恢复和完成会清除该原因。持久快照绝不包含激活态。
|
||||
|
||||
### 持久记录与回放
|
||||
|
||||
每次非清除变更都通过 `Agent.inject()` 追加一条原始且模型可见的 `context/message`,其中包含带版本的完整快照。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`;元数据必须与渲染后的 `<goal_state>...</goal_state>` 内容完全一致。会话日志是唯一的持久事实来源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。
|
||||
每次非清除变更都通过 `Agent.inject()` 追加一条原始且模型可见的 `context/message`,其中包含带版本的完整快照。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`;元数据必须与渲染后的 `<goal_state>...</goal_state>` 内容完全一致。这个描述性分隔符沿用了仓库已有的 `<workspace_context>` 约定,也符合 [Anthropic 关于用一致且描述明确的 XML 标签组织混合提示词内容的公开指南](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags)。这是公开的模型体验先例,并非对任何提供方专有训练语料的推断。会话日志是唯一的持久事实来源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。
|
||||
|
||||
回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
|
||||
|
||||
@@ -26,7 +26,7 @@ Status: implemented
|
||||
|
||||
### 生命周期与实时激活态
|
||||
|
||||
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`,陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时,停止阶段或已解除激活的活跃目标才能恢复;只有已接纳回合数达到上限后,才能标记预算受限。
|
||||
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`,陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费者。
|
||||
|
||||
从任何种子构建的缓存都以未激活状态开始,每次 `agent/session-start` 边沿也会再次解除激活。因此,恢复和 fork 会保留持久目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略表面可以显式调用恢复操作并激活目标。
|
||||
|
||||
@@ -36,7 +36,7 @@ Status: implemented
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动时解除激活与活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动时解除激活与活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -52,7 +52,7 @@ Status: implemented
|
||||
- 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
|
||||
- 完整快照便于检查和严格回放,但在压缩隐藏它们之前,会在模型历史中重复目标描述与状态字段。
|
||||
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
|
||||
- 回合上限只约束继续执行次数;token、费用、时间和提供方限制仍属于独立策略。
|
||||
- 回合上限只约束继续执行次数;当回合、token、费用、时间或提供方限制停止工作时,策略消费者会把它们映射为不同的阻塞原因。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
|
||||
@@ -471,7 +471,7 @@ Goal mutation accepted by one live agent. The matching context event is already
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [GoalChanged](../core-data-structures/goal.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/goal/goal/src/types.ts:166`](../../packages/goal/goal/src/types.ts)
|
||||
Source: [`packages/goal/goal/src/types.ts:167`](../../packages/goal/goal/src/types.ts)
|
||||
|
||||
## `llm/*`
|
||||
|
||||
|
||||
@@ -484,13 +484,6 @@ Source: [`packages/fs/fs/src/index.ts:80`](../../packages/fs/fs/src/index.ts)
|
||||
Goal service (`ctx.goals`) backed exclusively by the owning session log.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Materialize deployment defaults and validate one create request.
|
||||
* @param request - objective plus optional caller-selected round cap.
|
||||
* @returns detached, fully resolved create specification.
|
||||
*/
|
||||
resolveCreate(request: CreateGoalRequest): CreateGoalSpec
|
||||
|
||||
/**
|
||||
* Read the current goal for one exact live agent.
|
||||
* @param agent - owning live agent.
|
||||
@@ -546,25 +539,10 @@ complete(agent: Agent, ref: GoalRef): GoalView
|
||||
* Mark an active goal blocked and disarm it.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the blocked view.
|
||||
* @param reason - policy-owned stable code and human-readable explanation.
|
||||
* @returns the blocked view with its durable reason.
|
||||
*/
|
||||
block(agent: Agent, ref: GoalRef): GoalView
|
||||
|
||||
/**
|
||||
* Mark an active goal stopped by an external usage limit.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the usage-limited view.
|
||||
*/
|
||||
markUsageLimited(agent: Agent, ref: GoalRef): GoalView
|
||||
|
||||
/**
|
||||
* Mark an active goal stopped at its configured round cap.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the budget-limited view.
|
||||
*/
|
||||
markBudgetLimited(agent: Agent, ref: GoalRef): GoalView
|
||||
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView
|
||||
|
||||
/**
|
||||
* Clear the current goal while retaining a durable tombstone and history.
|
||||
@@ -575,9 +553,9 @@ markBudgetLimited(agent: Agent, ref: GoalRef): GoalView
|
||||
clear(agent: Agent, ref: GoalRef): GoalRef
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [CreateGoalRequest](../core-data-structures/goal.md) · [CreateGoalSpec](../core-data-structures/goal.md) · [EditGoalRequest](../core-data-structures/goal.md) · [GoalRef](../core-data-structures/goal.md) · [GoalView](../core-data-structures/goal.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [CreateGoalRequest](../core-data-structures/goal.md) · [EditGoalRequest](../core-data-structures/goal.md) · [GoalBlockReason](../core-data-structures/goal.md) · [GoalRef](../core-data-structures/goal.md) · [GoalView](../core-data-structures/goal.md)
|
||||
|
||||
Source: [`packages/goal/goal/src/index.ts:104`](../../packages/goal/goal/src/index.ts)
|
||||
Source: [`packages/goal/goal/src/index.ts:131`](../../packages/goal/goal/src/index.ts)
|
||||
|
||||
## `ctx.llm` — `LlmService`
|
||||
|
||||
|
||||
@@ -24,11 +24,21 @@ type GoalPhase =
|
||||
| 'active'
|
||||
| 'paused'
|
||||
| 'blocked'
|
||||
| 'usage-limited'
|
||||
| 'budget-limited'
|
||||
| 'complete'
|
||||
```
|
||||
|
||||
Blocking is the single durable stopped-by-a-problem state. Its policy-owned reason carries a stable lower-kebab-case code for routing and a free-form explanation for humans and models.
|
||||
|
||||
```ts type-equiv
|
||||
/** Machine-routable and human-readable explanation for a blocked goal. */
|
||||
interface GoalBlockReason {
|
||||
/** Stable lower-kebab-case classification chosen by the blocking policy. */
|
||||
readonly code: string
|
||||
/** Non-empty explanation shown to humans and models. */
|
||||
readonly message: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Full durable state written by every non-clear goal mutation. */
|
||||
interface GoalSnapshot extends GoalRef {
|
||||
@@ -36,6 +46,8 @@ interface GoalSnapshot extends GoalRef {
|
||||
readonly objective: string
|
||||
/** Durable lifecycle phase. */
|
||||
readonly phase: GoalPhase
|
||||
/** Present exactly while `phase` is `blocked`. */
|
||||
readonly blockedReason?: GoalBlockReason
|
||||
/** Total admitted goal-round cap. */
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
@@ -98,7 +110,7 @@ interface GoalMessageSource {
|
||||
|
||||
## Requests and notifications
|
||||
|
||||
Creation separates caller omission from the resolved deployment choice. An edit is a partial replacement whose runtime validator requires at least one field. Every mutation notification carries the accepted operation and exact revision; clear omits `goal`.
|
||||
Creation separates caller omission from the deployment choice, which `create()` resolves internally. An edit is a partial replacement whose runtime validator requires at least one field. Every mutation notification carries the accepted operation and exact revision; clear omits `goal`.
|
||||
|
||||
```ts type-equiv
|
||||
/** Input whose omitted round cap is resolved by the service configuration. */
|
||||
@@ -108,14 +120,6 @@ interface CreateGoalRequest {
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Validated create input with every deployment default materialized. */
|
||||
interface CreateGoalSpec {
|
||||
readonly objective: string
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Fields changed by an edit; at least one must be present. */
|
||||
interface EditGoalRequest {
|
||||
|
||||
@@ -27,7 +27,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:61`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:70`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:53`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/types.ts:166`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | - |
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/types.ts:167`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | - |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:43`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
|
||||
|
||||
## goal
|
||||
|
||||
- **goal** — one durable completion objective attached to an existing session, with a revisioned lifecycle phase and a goal-round cap. A goal is state, not a scheduler or a separate conversation; the session log remains its source of truth.
|
||||
- **goal** — one durable completion objective attached to an existing session, with a revisioned `active` / `paused` / `blocked` / `complete` phase and a goal-round cap; `blocked` retains a policy code and explanation. A goal is state, not a scheduler or a separate conversation; the session log remains its source of truth.
|
||||
- **goal round** — one continuation cycle admitted for the current goal. The same-session driver materializes a goal round as one goal-sourced [turn](#turn), which can contain multiple steps; unrelated human turns in the same session do not consume the goal-round cap. <a id="goal-round"></a>
|
||||
- **goal activation** — process-local permission for a continuation consumer to admit another goal round. Activation is either `armed` or `disarmed`; it is deliberately absent from durable replay, so resume and fork require a later explicit resume mutation before automatic work.
|
||||
|
||||
|
||||
@@ -254,10 +254,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
key: 'goals',
|
||||
summary: 'Goal service (`ctx.goals`) backed exclusively by the owning session log.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'resolveCreate(request: CreateGoalRequest): CreateGoalSpec',
|
||||
jsDoc: '/**\n * Materialize deployment defaults and validate one create request.\n * @param request - objective plus optional caller-selected round cap.\n * @returns detached, fully resolved create specification.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'get(agent: Agent): GoalView | undefined',
|
||||
jsDoc: '/**\n * Read the current goal for one exact live agent.\n * @param agent - owning live agent.\n * @returns a fresh view or `undefined` when no goal is current.\n * @throws {@link GoalError} when the agent is not the registry\'s live instance.\n */',
|
||||
@@ -283,16 +279,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
jsDoc: '/**\n * Mark a current non-complete goal complete and disarm it.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the completed view.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'block(agent: Agent, ref: GoalRef): GoalView',
|
||||
jsDoc: '/**\n * Mark an active goal blocked and disarm it.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the blocked view.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'markUsageLimited(agent: Agent, ref: GoalRef): GoalView',
|
||||
jsDoc: '/**\n * Mark an active goal stopped by an external usage limit.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the usage-limited view.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'markBudgetLimited(agent: Agent, ref: GoalRef): GoalView',
|
||||
jsDoc: '/**\n * Mark an active goal stopped at its configured round cap.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the budget-limited view.\n */',
|
||||
signature: 'block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView',
|
||||
jsDoc: '/**\n * Mark an active goal blocked and disarm it.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @param reason - policy-owned stable code and human-readable explanation.\n * @returns the blocked view with its durable reason.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'clear(agent: Agent, ref: GoalRef): GoalRef',
|
||||
@@ -1137,10 +1125,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'CreateGoalRequest',
|
||||
declaration: 'export interface CreateGoalRequest {\n readonly objective: string;\n readonly maxGoalRounds?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'CreateGoalSpec',
|
||||
declaration: 'export interface CreateGoalSpec {\n readonly objective: string;\n readonly maxGoalRounds: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'CreateSessionOptions',
|
||||
declaration: 'export interface CreateSessionOptions {\n readonly seed?: readonly SessionEvent[];\n readonly meta?: {\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly createdAt?: number;\n readonly seedLength?: number;\n };\n}',
|
||||
@@ -1241,13 +1225,17 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'GoalActivation',
|
||||
declaration: 'export type GoalActivation = \'armed\' | \'disarmed\';',
|
||||
},
|
||||
{
|
||||
name: 'GoalBlockReason',
|
||||
declaration: 'export interface GoalBlockReason {\n readonly code: string;\n readonly message: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'GoalId',
|
||||
declaration: 'export type GoalId = Branded<\'GoalId\'>;',
|
||||
},
|
||||
{
|
||||
name: 'GoalPhase',
|
||||
declaration: 'export type GoalPhase = \'active\' | \'paused\' | \'blocked\' | \'usage-limited\' | \'budget-limited\' | \'complete\';',
|
||||
declaration: 'export type GoalPhase = \'active\' | \'paused\' | \'blocked\' | \'complete\';',
|
||||
},
|
||||
{
|
||||
name: 'GoalRef',
|
||||
@@ -1255,7 +1243,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'GoalSnapshot',
|
||||
declaration: 'export interface GoalSnapshot extends GoalRef {\n readonly objective: string;\n readonly phase: GoalPhase;\n readonly maxGoalRounds: number;\n}',
|
||||
declaration: 'export interface GoalSnapshot extends GoalRef {\n readonly objective: string;\n readonly phase: GoalPhase;\n readonly blockedReason?: GoalBlockReason;\n readonly maxGoalRounds: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'GoalView',
|
||||
|
||||
@@ -11,13 +11,13 @@ Event-sourced same-session goal state. The service retains one current completio
|
||||
defaultMaxGoalRounds: 256
|
||||
```
|
||||
|
||||
`defaultMaxGoalRounds` must be a positive safe integer. `resolveCreate()` materializes this deployment default before `create()` commits a goal; a request-level value overrides it.
|
||||
`defaultMaxGoalRounds` must be a positive safe integer. `create()` materializes this deployment default internally before committing a goal; a request-level value overrides it.
|
||||
|
||||
## Service contract
|
||||
|
||||
`ctx.goals` accepts only the exact live `Agent` instance registered under its id. `get()` returns a detached `GoalView`; mutations use a `GoalRef { id, revision }` compare-and-set fence and reject stale refs. The service exposes create, edit, pause, resume, complete, block, usage-limit, budget-limit, and clear verbs through the generated [service catalog](../../../docs/cordis-catalog/services.md).
|
||||
`ctx.goals` accepts only the exact live `Agent` instance registered under its id. `get()` returns a detached `GoalView`; mutations use a `GoalRef { id, revision }` compare-and-set fence and reject stale refs. The service exposes create, edit, pause, resume, complete, block, and clear verbs through the generated [service catalog](../../../docs/cordis-catalog/services.md). Creation default resolution is an internal implementation step, not an additional public verb.
|
||||
|
||||
At most one goal is current. Creation produces an active revision-one goal and arms it. A non-complete goal must be edited, transitioned, or cleared; a completed goal may be replaced by a globally fresh id. Edits retain phase and activation. Pause, completion, blocking, limit transitions, and clear disarm activation. Resume accepts a stopped phase or a disarmed active goal only while the configured round cap has remaining capacity; an active armed goal rejects the redundant operation.
|
||||
At most one goal is current. Creation produces an active revision-one goal and arms it. A non-complete goal must be edited, transitioned, or cleared; a completed goal may be replaced by a globally fresh id. Edits retain phase, blocker reason, and activation. Pause, completion, blocking, and clear disarm activation. A block records a policy-owned lower-kebab-case code plus a normalized free-form explanation; provider limits, configured budgets, execution errors, and requests for human input all use this one durable phase rather than multiplying lifecycle states. Resume accepts a stopped phase or a disarmed active goal only while the configured round cap has remaining capacity; it clears any former blocker reason. An active armed goal rejects the redundant operation.
|
||||
|
||||
Every non-clear mutation appends a complete versioned snapshot through `agent.inject()`; clear appends a revisioned tombstone. The raw `context/message`, its `{ kind: 'goal' }` source, and its metadata must agree exactly. Replay rejects malformed shapes, source/content drift, discontinuous revisions, illegal lifecycle transitions, non-monotonic per-goal timestamps, and non-sequential goal rounds. Mutation timestamps clamp against the preceding goal update when wall time moves backward.
|
||||
|
||||
@@ -35,7 +35,7 @@ Policy plugins call the service verbs and react to the scoped `goal/changed` eve
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Each mutation is one raw user-role context block. A snapshot is rendered as `<goal_state>{"goal":...,"roundsStarted":...,"createdAt":...,"updatedAt":...}</goal_state>`; a clear renders the tombstone id/revision and `clearedAt`. There is no hidden state summary outside the log.
|
||||
Each mutation is one raw user-role context block. A snapshot is rendered as `<goal_state>{"goal":...,"roundsStarted":...,"createdAt":...,"updatedAt":...}</goal_state>`; a clear renders the tombstone id/revision and `clearedAt`. There is no hidden state summary outside the log. The descriptive XML delimiter follows this repository's existing `<workspace_context>` convention and [Anthropic's published XML-tag prompting guidance](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags); it is public model-experience prior art, not a claim about any provider's proprietary training corpus.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { renderGoalChange } from './render.ts'
|
||||
import { GOAL_CHANGE_VERSION, GoalId } from './runtime.ts'
|
||||
import type {
|
||||
FoldedGoal,
|
||||
GoalBlockReason,
|
||||
GoalChangeMeta,
|
||||
GoalClearChangeMeta,
|
||||
GoalMessageSource,
|
||||
@@ -25,17 +26,8 @@ const SNAPSHOT_OPERATIONS: ReadonlySet<Exclude<GoalOperation, 'clear'>> = new Se
|
||||
'resume',
|
||||
'complete',
|
||||
'block',
|
||||
'mark-usage-limited',
|
||||
'mark-budget-limited',
|
||||
])
|
||||
const PHASES: ReadonlySet<GoalPhase> = new Set([
|
||||
'active',
|
||||
'paused',
|
||||
'blocked',
|
||||
'usage-limited',
|
||||
'budget-limited',
|
||||
'complete',
|
||||
])
|
||||
const PHASES: ReadonlySet<GoalPhase> = new Set(['active', 'paused', 'blocked', 'complete'])
|
||||
|
||||
/** Mutable accumulator kept private to the pure fold. */
|
||||
export interface GoalFoldState {
|
||||
@@ -83,13 +75,24 @@ function nonNegativeInteger(value: unknown, field: string): number {
|
||||
return value
|
||||
}
|
||||
|
||||
/** Decode one canonical blocker explanation. */
|
||||
function decodeBlockReason(value: unknown): GoalBlockReason {
|
||||
if (!isRecord(value) || Object.keys(value).sort().join(',') !== 'code,message') {
|
||||
throw new Error('goal change goal.blockedReason has an invalid shape')
|
||||
}
|
||||
if (typeof value['code'] !== 'string' || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value['code'])) {
|
||||
throw new Error('goal change goal.blockedReason.code must be lower-kebab-case')
|
||||
}
|
||||
if (typeof value['message'] !== 'string' || value['message'].trim().length === 0
|
||||
|| value['message'] !== value['message'].trim()) {
|
||||
throw new Error('goal change goal.blockedReason.message must be non-empty and normalized')
|
||||
}
|
||||
return { code: value['code'], message: value['message'] }
|
||||
}
|
||||
|
||||
/** Decode and validate one snapshot. */
|
||||
function decodeSnapshot(value: unknown): GoalSnapshot {
|
||||
if (!isRecord(value)) throw new Error('goal change goal must be a record')
|
||||
const keys = Object.keys(value).sort()
|
||||
if (keys.join(',') !== 'id,maxGoalRounds,objective,phase,revision') {
|
||||
throw new Error('goal change goal has an invalid shape')
|
||||
}
|
||||
if (typeof value['id'] !== 'string' || value['id'].length === 0) {
|
||||
throw new Error('goal change goal.id must be a non-empty string')
|
||||
}
|
||||
@@ -100,12 +103,20 @@ function decodeSnapshot(value: unknown): GoalSnapshot {
|
||||
if (typeof value['phase'] !== 'string' || !PHASES.has(value['phase'] as GoalPhase)) {
|
||||
throw new Error('goal change goal.phase is invalid')
|
||||
}
|
||||
const phase = value['phase'] as GoalPhase
|
||||
const expectedKeys = phase === 'blocked'
|
||||
? 'blockedReason,id,maxGoalRounds,objective,phase,revision'
|
||||
: 'id,maxGoalRounds,objective,phase,revision'
|
||||
if (Object.keys(value).sort().join(',') !== expectedKeys) {
|
||||
throw new Error('goal change goal has an invalid shape')
|
||||
}
|
||||
return {
|
||||
id: GoalId(value['id']),
|
||||
revision: positiveInteger(value['revision'], 'goal.revision'),
|
||||
objective: value['objective'],
|
||||
phase: value['phase'] as GoalPhase,
|
||||
phase,
|
||||
maxGoalRounds: positiveInteger(value['maxGoalRounds'], 'goal.maxGoalRounds'),
|
||||
...phase === 'blocked' ? { blockedReason: decodeBlockReason(value['blockedReason']) } : {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +219,10 @@ function validateSnapshotTransition(
|
||||
}
|
||||
switch (change.operation) {
|
||||
case 'edit':
|
||||
if (next.phase !== current.phase) throw new Error('goal edit cannot change phase')
|
||||
if (next.phase !== current.phase
|
||||
|| JSON.stringify(next.blockedReason) !== JSON.stringify(current.blockedReason)) {
|
||||
throw new Error('goal edit cannot change phase or blocked reason')
|
||||
}
|
||||
break
|
||||
case 'pause':
|
||||
requireSameDefinition(current, next, change.operation)
|
||||
@@ -220,8 +234,6 @@ function validateSnapshotTransition(
|
||||
'active',
|
||||
'paused',
|
||||
'blocked',
|
||||
'usage-limited',
|
||||
'budget-limited',
|
||||
])
|
||||
if (!resumable.has(current.phase) || next.phase !== 'active' || state.roundsStarted >= next.maxGoalRounds) {
|
||||
throw new Error('goal resume has an invalid phase transition or exhausted round budget')
|
||||
@@ -236,19 +248,6 @@ function validateSnapshotTransition(
|
||||
requireSameDefinition(current, next, change.operation)
|
||||
if (current.phase !== 'active' || next.phase !== 'blocked') throw new Error('goal block has an invalid phase transition')
|
||||
break
|
||||
case 'mark-usage-limited':
|
||||
requireSameDefinition(current, next, change.operation)
|
||||
if (current.phase !== 'active' || next.phase !== 'usage-limited') {
|
||||
throw new Error('goal mark-usage-limited has an invalid phase transition')
|
||||
}
|
||||
break
|
||||
case 'mark-budget-limited':
|
||||
requireSameDefinition(current, next, change.operation)
|
||||
if (current.phase !== 'active' || next.phase !== 'budget-limited'
|
||||
|| state.roundsStarted < next.maxGoalRounds) {
|
||||
throw new Error('goal mark-budget-limited has an invalid phase transition or remaining round budget')
|
||||
}
|
||||
break
|
||||
/* v8 ignore start -- the caller excludes create and GoalOperation is closed; these arms retain fail-loud exhaustiveness */
|
||||
case 'create':
|
||||
throw new Error('goal create cannot be validated as a current-goal transition')
|
||||
|
||||
@@ -27,9 +27,9 @@ import {
|
||||
} from './runtime.ts'
|
||||
import type {
|
||||
CreateGoalRequest,
|
||||
CreateGoalSpec,
|
||||
EditGoalRequest,
|
||||
GoalActivation,
|
||||
GoalBlockReason,
|
||||
GoalChangeMeta,
|
||||
GoalChanged,
|
||||
GoalClearChangeMeta,
|
||||
@@ -79,6 +79,12 @@ interface GoalCache {
|
||||
readonly pending: PendingGoalChange[]
|
||||
}
|
||||
|
||||
/** Validated create input with every deployment default materialized. */
|
||||
interface ResolvedCreateGoal {
|
||||
readonly objective: string
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
|
||||
/** Validate a caller-visible positive safe-integer round cap. */
|
||||
function resolveMaxGoalRounds(value: number): number {
|
||||
if (!Number.isSafeInteger(value) || value < 1) {
|
||||
@@ -95,6 +101,31 @@ function resolveObjective(value: string): string {
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
/** Materialize deployment defaults and validate one create request. */
|
||||
function resolveCreateGoal(request: CreateGoalRequest, defaultMaxGoalRounds: number): ResolvedCreateGoal {
|
||||
return {
|
||||
objective: resolveObjective(request.objective),
|
||||
maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds ?? defaultMaxGoalRounds),
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate and detach one policy-owned blocker explanation. */
|
||||
function resolveBlockReason(reason: unknown): GoalBlockReason {
|
||||
const record = typeof reason === 'object' && reason !== null && !Array.isArray(reason)
|
||||
? reason as Record<string, unknown>
|
||||
: undefined
|
||||
const code = record?.['code']
|
||||
const message = record?.['message']
|
||||
if (typeof code !== 'string' || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(code)
|
||||
|| typeof message !== 'string' || message.trim().length === 0) {
|
||||
throw new GoalError(
|
||||
'goal block reason requires a lower-kebab-case code and a non-empty message',
|
||||
'GOAL_INVALID_BLOCK_REASON',
|
||||
)
|
||||
}
|
||||
return { code, message: message.trim() }
|
||||
}
|
||||
|
||||
/** Compare the complete canonical payloads used for deferred reconciliation. */
|
||||
function sameChange(left: GoalChangeMeta, right: GoalChangeMeta): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right)
|
||||
@@ -121,18 +152,6 @@ export class GoalService extends Service {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize deployment defaults and validate one create request.
|
||||
* @param request - objective plus optional caller-selected round cap.
|
||||
* @returns detached, fully resolved create specification.
|
||||
*/
|
||||
resolveCreate(request: CreateGoalRequest): CreateGoalSpec {
|
||||
return {
|
||||
objective: resolveObjective(request.objective),
|
||||
maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds ?? this.resolved.defaultMaxGoalRounds),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current goal for one exact live agent.
|
||||
* @param agent - owning live agent.
|
||||
@@ -154,7 +173,7 @@ export class GoalService extends Service {
|
||||
* @returns the created live view.
|
||||
*/
|
||||
create(agent: Agent, request: CreateGoalRequest): GoalView {
|
||||
const spec = this.resolveCreate(request)
|
||||
const spec = resolveCreateGoal(request, this.resolved.defaultMaxGoalRounds)
|
||||
const cache = this.prepareMutation(agent)
|
||||
const current = cache.state.goal
|
||||
if (current !== undefined && current.phase !== 'complete') {
|
||||
@@ -213,7 +232,7 @@ export class GoalService extends Service {
|
||||
resume(agent: Agent, ref: GoalRef): GoalView {
|
||||
const cache = this.prepareMutation(agent)
|
||||
const current = this.expectCurrent(cache, ref)
|
||||
const resumable: readonly GoalPhase[] = ['active', 'paused', 'blocked', 'usage-limited', 'budget-limited']
|
||||
const resumable: readonly GoalPhase[] = ['active', 'paused', 'blocked']
|
||||
if (!resumable.includes(current.phase)) {
|
||||
throw this.transitionError(current, 'resume', resumable)
|
||||
}
|
||||
@@ -240,7 +259,7 @@ export class GoalService extends Service {
|
||||
agent,
|
||||
ref,
|
||||
'complete',
|
||||
['active', 'paused', 'blocked', 'usage-limited', 'budget-limited'],
|
||||
['active', 'paused', 'blocked'],
|
||||
'complete',
|
||||
'disarmed',
|
||||
)
|
||||
@@ -250,45 +269,20 @@ export class GoalService extends Service {
|
||||
* Mark an active goal blocked and disarm it.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the blocked view.
|
||||
* @param reason - policy-owned stable code and human-readable explanation.
|
||||
* @returns the blocked view with its durable reason.
|
||||
*/
|
||||
block(agent: Agent, ref: GoalRef): GoalView {
|
||||
return this.transition(agent, ref, 'block', ['active'], 'blocked', 'disarmed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an active goal stopped by an external usage limit.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the usage-limited view.
|
||||
*/
|
||||
markUsageLimited(agent: Agent, ref: GoalRef): GoalView {
|
||||
return this.transition(agent, ref, 'mark-usage-limited', ['active'], 'usage-limited', 'disarmed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an active goal stopped at its configured round cap.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the budget-limited view.
|
||||
*/
|
||||
markBudgetLimited(agent: Agent, ref: GoalRef): GoalView {
|
||||
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView {
|
||||
const cache = this.prepareMutation(agent)
|
||||
const current = this.expectCurrent(cache, ref)
|
||||
if (current.phase !== 'active') {
|
||||
throw this.transitionError(current, 'mark-budget-limited', ['active'])
|
||||
}
|
||||
if (cache.state.roundsStarted < current.maxGoalRounds) {
|
||||
throw new GoalError(
|
||||
`goal "${current.id}" has started ${cache.state.roundsStarted}/${current.maxGoalRounds} rounds`,
|
||||
'GOAL_INVALID_TRANSITION',
|
||||
)
|
||||
throw this.transitionError(current, 'block', ['active'])
|
||||
}
|
||||
return this.commitCurrent(
|
||||
agent,
|
||||
cache,
|
||||
'mark-budget-limited',
|
||||
this.withPhase(current, 'budget-limited'),
|
||||
'block',
|
||||
{ ...this.withPhase(current, 'blocked'), blockedReason: resolveBlockReason(reason) },
|
||||
'disarmed',
|
||||
)
|
||||
}
|
||||
@@ -384,7 +378,13 @@ export class GoalService extends Service {
|
||||
|
||||
/** Build a new revision with one replacement phase. */
|
||||
private withPhase(current: GoalSnapshot, phase: GoalPhase): GoalSnapshot {
|
||||
return { ...current, revision: current.revision + 1, phase }
|
||||
return {
|
||||
id: current.id,
|
||||
revision: current.revision + 1,
|
||||
objective: current.objective,
|
||||
phase,
|
||||
maxGoalRounds: current.maxGoalRounds,
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared validated phase transition. */
|
||||
|
||||
@@ -22,16 +22,24 @@ export type GoalPhase =
|
||||
| 'active'
|
||||
| 'paused'
|
||||
| 'blocked'
|
||||
| 'usage-limited'
|
||||
| 'budget-limited'
|
||||
| 'complete'
|
||||
|
||||
/** Machine-routable and human-readable explanation for a blocked goal. */
|
||||
export interface GoalBlockReason {
|
||||
/** Stable lower-kebab-case classification chosen by the blocking policy. */
|
||||
readonly code: string
|
||||
/** Non-empty explanation shown to humans and models. */
|
||||
readonly message: string
|
||||
}
|
||||
|
||||
/** Full durable state written by every non-clear goal mutation. */
|
||||
export interface GoalSnapshot extends GoalRef {
|
||||
/** Human-requested completion objective. */
|
||||
readonly objective: string
|
||||
/** Durable lifecycle phase. */
|
||||
readonly phase: GoalPhase
|
||||
/** Present exactly while `phase` is `blocked`. */
|
||||
readonly blockedReason?: GoalBlockReason
|
||||
/** Total admitted goal-round cap. */
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
@@ -59,8 +67,6 @@ export type GoalOperation =
|
||||
| 'resume'
|
||||
| 'complete'
|
||||
| 'block'
|
||||
| 'mark-usage-limited'
|
||||
| 'mark-budget-limited'
|
||||
| 'clear'
|
||||
|
||||
/** Full-snapshot goal mutation retained in a model-visible context event. */
|
||||
@@ -121,12 +127,6 @@ export interface CreateGoalRequest {
|
||||
readonly maxGoalRounds?: number
|
||||
}
|
||||
|
||||
/** Validated create input with every deployment default materialized. */
|
||||
export interface CreateGoalSpec {
|
||||
readonly objective: string
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
|
||||
/** Fields changed by an edit; at least one must be present. */
|
||||
export interface EditGoalRequest {
|
||||
readonly objective?: string
|
||||
@@ -149,6 +149,7 @@ export type GoalErrorCode =
|
||||
| 'GOAL_STALE_REVISION'
|
||||
| 'GOAL_INVALID_OBJECTIVE'
|
||||
| 'GOAL_INVALID_MAX_ROUNDS'
|
||||
| 'GOAL_INVALID_BLOCK_REASON'
|
||||
| 'GOAL_INVALID_EDIT'
|
||||
| 'GOAL_INVALID_TRANSITION'
|
||||
|
||||
|
||||
@@ -111,17 +111,13 @@ function appendRound(session: Session, ref: GoalRef, round: number): void {
|
||||
}
|
||||
|
||||
describe('GoalService creation and replay', () => {
|
||||
it('resolves the configured default and writes one balanced raw context snapshot', async () => {
|
||||
it('applies the configured default and writes one balanced raw context snapshot', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(1_700_000_000_000)
|
||||
const { ctx, agent, session } = await harness({ defaultMaxGoalRounds: 17 })
|
||||
const seen: string[] = []
|
||||
ctx.on('goal/changed', (_subject, change) => { seen.push(change.operation) })
|
||||
|
||||
expect(ctx.goals.resolveCreate({ objective: ' finish the feature ' })).toEqual({
|
||||
objective: 'finish the feature',
|
||||
maxGoalRounds: 17,
|
||||
})
|
||||
const goal = ctx.goals.create(agent, { objective: ' finish the feature ' })
|
||||
|
||||
expect(goal).toMatchObject({
|
||||
@@ -151,18 +147,19 @@ describe('GoalService creation and replay', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('uses 256 rounds by default and validates create input at the owning resolver', async () => {
|
||||
it('uses 256 rounds by default and validates create input inside create', async () => {
|
||||
const { ctx, agent } = await harness()
|
||||
expect(ctx.goals.resolveCreate({ objective: 'x' })).toEqual({ objective: 'x', maxGoalRounds: 256 })
|
||||
expect(() => ctx.goals.resolveCreate({ objective: ' ' })).toThrow(expect.objectContaining({
|
||||
expect(() => ctx.goals.create(agent, { objective: ' ' })).toThrow(expect.objectContaining({
|
||||
code: 'GOAL_INVALID_OBJECTIVE',
|
||||
}))
|
||||
expect(() => ctx.goals.resolveCreate({ objective: 'x', maxGoalRounds: 0 })).toThrow(expect.objectContaining({
|
||||
expect(() => ctx.goals.create(agent, { objective: 'x', maxGoalRounds: 0 })).toThrow(expect.objectContaining({
|
||||
code: 'GOAL_INVALID_MAX_ROUNDS',
|
||||
}))
|
||||
expect(() => ctx.goals.resolveCreate({ objective: 'x', maxGoalRounds: 1.5 })).toThrow(GoalError)
|
||||
expect(() => ctx.goals.resolveCreate({ objective: 'x', maxGoalRounds: 1.5 })).toThrow(HarnessError)
|
||||
expect(() => ctx.goals.resolveCreate({ objective: 'x', maxGoalRounds: Number.MAX_SAFE_INTEGER + 1 })).toThrow(GoalError)
|
||||
expect(() => ctx.goals.create(agent, { objective: 'x', maxGoalRounds: 1.5 })).toThrow(GoalError)
|
||||
expect(() => ctx.goals.create(agent, { objective: 'x', maxGoalRounds: 1.5 })).toThrow(HarnessError)
|
||||
expect(() => ctx.goals.create(agent, {
|
||||
objective: 'x', maxGoalRounds: Number.MAX_SAFE_INTEGER + 1,
|
||||
})).toThrow(GoalError)
|
||||
expect(ctx.goals.create(agent, { objective: 'x' }).maxGoalRounds).toBe(256)
|
||||
})
|
||||
|
||||
@@ -170,9 +167,10 @@ describe('GoalService creation and replay', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const goals = new GoalService(ctx)
|
||||
expect(goals.resolveCreate({ objective: 'direct' })).toEqual({
|
||||
objective: 'direct',
|
||||
maxGoalRounds: 256,
|
||||
const stub = stubAgent('goal-direct-construction')
|
||||
ctx.agents.register(stub.agent)
|
||||
expect(goals.create(stub.agent, { objective: 'direct' })).toMatchObject({
|
||||
objective: 'direct', maxGoalRounds: 256,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -287,18 +285,19 @@ describe('GoalService mutations', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('supports pause, resume, block, usage-limit, and completion transitions', async () => {
|
||||
it('supports pause, resume, block, and completion transitions', async () => {
|
||||
const { ctx, agent } = await harness()
|
||||
let goal = ctx.goals.create(agent, { objective: 'lifecycle' })
|
||||
goal = ctx.goals.pause(agent, goal)
|
||||
expect(goal).toMatchObject({ phase: 'paused', activation: 'disarmed', revision: 2 })
|
||||
goal = ctx.goals.resume(agent, goal)
|
||||
expect(goal).toMatchObject({ phase: 'active', activation: 'armed', revision: 3 })
|
||||
goal = ctx.goals.block(agent, goal)
|
||||
expect(goal).toMatchObject({ phase: 'blocked', activation: 'disarmed' })
|
||||
goal = ctx.goals.resume(agent, goal)
|
||||
goal = ctx.goals.markUsageLimited(agent, goal)
|
||||
expect(goal.phase).toBe('usage-limited')
|
||||
goal = ctx.goals.block(agent, goal, { code: 'needs-input', message: 'A choice is required.' })
|
||||
expect(goal).toMatchObject({
|
||||
phase: 'blocked',
|
||||
blockedReason: { code: 'needs-input', message: 'A choice is required.' },
|
||||
activation: 'disarmed',
|
||||
})
|
||||
goal = ctx.goals.resume(agent, goal)
|
||||
goal = ctx.goals.pause(agent, goal)
|
||||
goal = ctx.goals.complete(agent, goal)
|
||||
@@ -307,15 +306,13 @@ describe('GoalService mutations', () => {
|
||||
})
|
||||
|
||||
it('allows completion from every stopped phase and replacement only after completion', async () => {
|
||||
const phases = ['paused', 'blocked', 'usage-limited'] as const
|
||||
const phases = ['paused', 'blocked'] as const
|
||||
for (const phase of phases) {
|
||||
const { ctx, agent } = await harness()
|
||||
let goal = ctx.goals.create(agent, { objective: phase })
|
||||
goal = phase === 'paused'
|
||||
? ctx.goals.pause(agent, goal)
|
||||
: phase === 'blocked'
|
||||
? ctx.goals.block(agent, goal)
|
||||
: ctx.goals.markUsageLimited(agent, goal)
|
||||
: ctx.goals.block(agent, goal, { code: 'test-blocker', message: 'Blocked for the test.' })
|
||||
const complete = ctx.goals.complete(agent, goal)
|
||||
const replacement = ctx.goals.create(agent, { objective: `after ${phase}` })
|
||||
expect(complete.phase).toBe('complete')
|
||||
@@ -333,32 +330,45 @@ describe('GoalService mutations', () => {
|
||||
expect(() => ctx.goals.resume(agent, goal)).toThrow(expect.objectContaining({ code: 'GOAL_INVALID_TRANSITION' }))
|
||||
const paused = ctx.goals.pause(agent, goal)
|
||||
expect(() => ctx.goals.pause(agent, paused)).toThrow(expect.objectContaining({ code: 'GOAL_INVALID_TRANSITION' }))
|
||||
expect(() => ctx.goals.block(agent, paused)).toThrow(expect.objectContaining({ code: 'GOAL_INVALID_TRANSITION' }))
|
||||
expect(() => ctx.goals.markUsageLimited(agent, paused)).toThrow(expect.objectContaining({
|
||||
code: 'GOAL_INVALID_TRANSITION',
|
||||
}))
|
||||
expect(() => ctx.goals.markBudgetLimited(agent, paused)).toThrow(expect.objectContaining({
|
||||
expect(() => ctx.goals.block(agent, paused, {
|
||||
code: 'test-blocker', message: 'Blocked for the test.',
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: 'GOAL_INVALID_TRANSITION',
|
||||
}))
|
||||
})
|
||||
|
||||
it('enforces the goal-round cap before budget limiting and resuming', async () => {
|
||||
it('records canonical blocker reasons and enforces the round cap on resume', async () => {
|
||||
const { ctx, agent, session } = await harness()
|
||||
let goal = ctx.goals.create(agent, { objective: 'bounded', maxGoalRounds: 2 })
|
||||
for (const reason of [null, [], { code: 1, message: 'invalid code' }, { code: 'round-limit', message: 1 }]) {
|
||||
expect(() => ctx.goals.block(agent, goal, reason as never)).toThrow(expect.objectContaining({
|
||||
code: 'GOAL_INVALID_BLOCK_REASON',
|
||||
}))
|
||||
}
|
||||
expect(() => ctx.goals.block(agent, goal, {
|
||||
code: 'Not Canonical', message: 'invalid code',
|
||||
})).toThrow(expect.objectContaining({ code: 'GOAL_INVALID_BLOCK_REASON' }))
|
||||
expect(() => ctx.goals.block(agent, goal, {
|
||||
code: 'round-limit', message: ' ',
|
||||
})).toThrow(expect.objectContaining({ code: 'GOAL_INVALID_BLOCK_REASON' }))
|
||||
appendRound(session, goal, 1)
|
||||
expect(ctx.goals.get(agent)?.roundsStarted).toBe(1)
|
||||
expect(() => ctx.goals.markBudgetLimited(agent, goal)).toThrow(expect.objectContaining({
|
||||
code: 'GOAL_INVALID_TRANSITION',
|
||||
}))
|
||||
appendRound(session, goal, 2)
|
||||
goal = ctx.goals.markBudgetLimited(agent, goal)
|
||||
expect(goal).toMatchObject({ phase: 'budget-limited', roundsStarted: 2, activation: 'disarmed' })
|
||||
goal = ctx.goals.block(agent, goal, { code: 'round-limit', message: ' Goal round limit reached. ' })
|
||||
expect(goal).toMatchObject({
|
||||
phase: 'blocked',
|
||||
blockedReason: { code: 'round-limit', message: 'Goal round limit reached.' },
|
||||
roundsStarted: 2,
|
||||
activation: 'disarmed',
|
||||
})
|
||||
expect(() => ctx.goals.resume(agent, goal)).toThrow(expect.objectContaining({ code: 'GOAL_INVALID_TRANSITION' }))
|
||||
goal = ctx.goals.edit(agent, goal, { maxGoalRounds: 3 })
|
||||
expect(goal.blockedReason).toEqual({ code: 'round-limit', message: 'Goal round limit reached.' })
|
||||
goal = ctx.goals.resume(agent, goal)
|
||||
expect(goal).toMatchObject({ phase: 'active', maxGoalRounds: 3, activation: 'armed' })
|
||||
expect(goal.blockedReason).toBeUndefined()
|
||||
appendRound(session, goal, 3)
|
||||
goal = ctx.goals.markBudgetLimited(agent, goal)
|
||||
goal = ctx.goals.block(agent, goal, { code: 'round-limit', message: 'Goal round limit reached.' })
|
||||
expect(ctx.goals.complete(agent, goal).phase).toBe('complete')
|
||||
})
|
||||
|
||||
@@ -598,7 +608,16 @@ describe('goal replay validation', () => {
|
||||
return {
|
||||
...current,
|
||||
operation,
|
||||
goal: { ...current.goal, revision: current.goal.revision + 1, phase },
|
||||
goal: {
|
||||
id: current.goal.id,
|
||||
revision: current.goal.revision + 1,
|
||||
objective: current.goal.objective,
|
||||
phase,
|
||||
...phase === 'blocked'
|
||||
? { blockedReason: { code: 'test-blocker', message: 'Blocked for replay validation.' } }
|
||||
: {},
|
||||
maxGoalRounds: current.goal.maxGoalRounds,
|
||||
},
|
||||
updatedAt: current.updatedAt + 1,
|
||||
...overrides,
|
||||
}
|
||||
@@ -694,9 +713,6 @@ describe('goal replay validation', () => {
|
||||
mutation(base, 'resume', 'paused'),
|
||||
mutation(base, 'complete', 'active'),
|
||||
mutation(base, 'block', 'active'),
|
||||
mutation(base, 'mark-usage-limited', 'active'),
|
||||
mutation(base, 'mark-budget-limited', 'active'),
|
||||
mutation(base, 'mark-budget-limited', 'budget-limited'),
|
||||
]
|
||||
for (const change of invalid) expect(() => foldPair(base, change)).toThrow()
|
||||
|
||||
@@ -782,6 +798,12 @@ describe('goal replay validation', () => {
|
||||
{ ...base.goal, objective: ' ' },
|
||||
{ ...base.goal, objective: ' padded ' },
|
||||
{ ...base.goal, phase: 'unknown' },
|
||||
{ ...base.goal, blockedReason: { code: 'unexpected', message: 'Only blocked goals have reasons.' } },
|
||||
{ ...base.goal, phase: 'blocked' },
|
||||
{ ...base.goal, phase: 'blocked', blockedReason: null },
|
||||
{ ...base.goal, phase: 'blocked', blockedReason: { code: 'test-blocker', message: 'Valid.', extra: true } },
|
||||
{ ...base.goal, phase: 'blocked', blockedReason: { code: 'NOT_CANONICAL', message: 'Bad code.' } },
|
||||
{ ...base.goal, phase: 'blocked', blockedReason: { code: 'test-blocker', message: ' padded ' } },
|
||||
{ ...base.goal, revision: 0 },
|
||||
{ ...base.goal, maxGoalRounds: -1 },
|
||||
]
|
||||
|
||||
@@ -69,8 +69,8 @@ export const LINK_MAP: Record<string, string> = {
|
||||
FsWriteIntent: 'filesystem.md',
|
||||
FsWriteOutcome: 'filesystem.md',
|
||||
CreateGoalRequest: 'goal.md',
|
||||
CreateGoalSpec: 'goal.md',
|
||||
EditGoalRequest: 'goal.md',
|
||||
GoalBlockReason: 'goal.md',
|
||||
GoalChanged: 'goal.md',
|
||||
GoalRef: 'goal.md',
|
||||
GoalView: 'goal.md',
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalRef", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalPhase", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalBlockReason", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalSnapshot", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalView", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalSnapshotChangeMeta", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalClearChangeMeta", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalMessageSource", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "CreateGoalRequest", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "CreateGoalSpec", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "EditGoalRequest", "source": "packages/goal/goal/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/goal.md", "symbol": "GoalChanged", "source": "packages/goal/goal/src/types.ts" },
|
||||
|
||||
|
||||
@@ -543,7 +543,7 @@ Goal mutation accepted by one live agent. The matching context event is already
|
||||
- `agent` — agent whose session owns the goal.
|
||||
- `change` — fresh current projection or clear tombstone.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/types.ts#L166)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/types.ts#L167)
|
||||
|
||||
## llm/*
|
||||
|
||||
|
||||
@@ -6,26 +6,7 @@
|
||||
|
||||
Goal service (`ctx.goals`) backed exclusively by the owning session log.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L104)
|
||||
|
||||
### ctx.goals.resolveCreate(request)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Materialize deployment defaults and validate one create request.
|
||||
* @param request - objective plus optional caller-selected round cap.
|
||||
* @returns detached, fully resolved create specification.
|
||||
*/
|
||||
resolveCreate(request: CreateGoalRequest): CreateGoalSpec
|
||||
```
|
||||
|
||||
Materialize deployment defaults and validate one create request.
|
||||
|
||||
- `request` — objective plus optional caller-selected round cap.
|
||||
|
||||
**Returns** detached, fully resolved create specification.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L129)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L131)
|
||||
|
||||
### ctx.goals.get(agent)
|
||||
|
||||
@@ -45,7 +26,7 @@ Read the current goal for one exact live agent.
|
||||
|
||||
**Returns** a fresh view or `undefined` when no goal is current.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L142)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L157)
|
||||
|
||||
### ctx.goals.create(agent, request)
|
||||
|
||||
@@ -67,7 +48,7 @@ Create and arm a goal. A completed goal may be replaced; every other current pha
|
||||
|
||||
**Returns** the created live view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L156)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L171)
|
||||
|
||||
### ctx.goals.edit(agent, ref, request)
|
||||
|
||||
@@ -90,7 +71,7 @@ Edit objective and/or round cap without changing phase.
|
||||
|
||||
**Returns** the edited view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L181)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L196)
|
||||
|
||||
### ctx.goals.pause(agent, ref)
|
||||
|
||||
@@ -111,7 +92,7 @@ Pause an active goal and disarm automatic continuation.
|
||||
|
||||
**Returns** the paused view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L202)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L217)
|
||||
|
||||
### ctx.goals.resume(agent, ref)
|
||||
|
||||
@@ -133,7 +114,7 @@ Resume and arm a stopped goal, or rearm an active goal after a session-start edg
|
||||
|
||||
**Returns** the active view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L213)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L228)
|
||||
|
||||
### ctx.goals.complete(agent, ref)
|
||||
|
||||
@@ -154,70 +135,30 @@ Mark a current non-complete goal complete and disarm it.
|
||||
|
||||
**Returns** the completed view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L238)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L253)
|
||||
|
||||
### ctx.goals.block(agent, ref)
|
||||
### ctx.goals.block(agent, ref, reason)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Mark an active goal blocked and disarm it.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the blocked view.
|
||||
* @param reason - policy-owned stable code and human-readable explanation.
|
||||
* @returns the blocked view with its durable reason.
|
||||
*/
|
||||
block(agent: Agent, ref: GoalRef): GoalView
|
||||
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView
|
||||
```
|
||||
|
||||
Mark an active goal blocked and disarm it.
|
||||
|
||||
- `agent` — owning live agent.
|
||||
- `ref` — expected current revision.
|
||||
- `reason` — policy-owned stable code and human-readable explanation.
|
||||
|
||||
**Returns** the blocked view.
|
||||
**Returns** the blocked view with its durable reason.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L255)
|
||||
|
||||
### ctx.goals.markUsageLimited(agent, ref)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Mark an active goal stopped by an external usage limit.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the usage-limited view.
|
||||
*/
|
||||
markUsageLimited(agent: Agent, ref: GoalRef): GoalView
|
||||
```
|
||||
|
||||
Mark an active goal stopped by an external usage limit.
|
||||
|
||||
- `agent` — owning live agent.
|
||||
- `ref` — expected current revision.
|
||||
|
||||
**Returns** the usage-limited view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L265)
|
||||
|
||||
### ctx.goals.markBudgetLimited(agent, ref)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Mark an active goal stopped at its configured round cap.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the budget-limited view.
|
||||
*/
|
||||
markBudgetLimited(agent: Agent, ref: GoalRef): GoalView
|
||||
```
|
||||
|
||||
Mark an active goal stopped at its configured round cap.
|
||||
|
||||
- `agent` — owning live agent.
|
||||
- `ref` — expected current revision.
|
||||
|
||||
**Returns** the budget-limited view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L275)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L271)
|
||||
|
||||
### ctx.goals.clear(agent, ref)
|
||||
|
||||
@@ -238,4 +179,4 @@ Clear the current goal while retaining a durable tombstone and history.
|
||||
|
||||
**Returns** the tombstone ref whose revision is one past the cleared snapshot.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L302)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L292)
|
||||
Reference in New Issue
Block a user