feat(goal): add persisted same-session goal domain
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# DeepSeek Harness Architecture
|
||||
|
||||
The **DeepSeek Harness SDK** builds agent harnesses on Cordis. The principle is simple: **everything is a plugin**. The shipped loop is one plugin, not a privileged kernel.
|
||||
The **DeepSeek Harness SDK** builds on Cordis: **everything is a plugin**, including the shipped loop.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -35,6 +35,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages add services (`ctx
|
||||
| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers |
|
||||
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry + generic `task_*` control tools |
|
||||
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
|
||||
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | persisted same-session goals |
|
||||
| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable storage for session logs |
|
||||
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred logical-corpus exact reads and relationship traces |
|
||||
|
||||
@@ -54,9 +55,9 @@ Waterfall events behave like around-middleware: a listener delegates by calling
|
||||
|
||||
## Default Loop Lifecycle
|
||||
|
||||
The shipped loop drains work from prompt through checkpoint. Every pause is a service call or event available to plugins.
|
||||
The shipped loop drains prompts through checkpoints; every pause is exposed to plugins through services or events.
|
||||
|
||||
A **session** is an append-only event log. A **turn** drains queued input until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points.
|
||||
A **turn** drains queued input in an append-only session until the model requests no more tools or plugin continuation. A **step** is one model request plus its tool executions. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points.
|
||||
|
||||
Startup resolves identity. No id mints `<config-id>-session-<uuid>`; `sessionId` resumes or creates; `resumeSessionId` requires history. Active failures emit `agent-loop/config-start-failed(sessionId, error)`, so front doors reject work; teardown stays silent.
|
||||
|
||||
@@ -108,7 +109,7 @@ forever:
|
||||
|
||||
Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variables; unknown or valueless references fail the turn. `dsh-system-prompt` owns the harness identity and default persona, which an agent scope may shadow. The loop supplies `model` and `cwd` ([prompt-ownership RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)).
|
||||
|
||||
Tool-time context—including async `agent.inject()` notices and post-tool `additionalContexts`—settles, then follows recorded results. Steering drains before `agent/post-step`, which observes durable output, results, context, and steering before signal closure. Leftovers become queued input. Terminal `agent/turn-stop` runs after continuation and steering folding, stays authoritative through turn close and flush, and discards later steering but preserves queued prompts.
|
||||
Tool-time context—including async `agent.inject()` notices and post-tool `additionalContexts`—settles after recorded results. Steering drains before `agent/post-step`, which observes durable output, results, context, and steering before signal closure. Leftovers become queued input. Terminal `agent/turn-stop` runs after continuation and steering folding, remains authoritative through turn close and flush, and discards later steering while preserving queued prompts.
|
||||
|
||||
`dsh-compact-basic` handles pressure and canonical overflow at these checkpoints; retry requires a balanced surface replacement ([RFC](rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)).
|
||||
|
||||
@@ -174,6 +175,7 @@ New behavior should attach to a documented extension point; changing the shipped
|
||||
| Add a session-stable request prefix outside history | compose it on `agent/session-prefix`, once per loop instance; logged on the request header |
|
||||
| Add UI or editor integration | drive `ctx.agents` and render from `session/event` |
|
||||
| Add durable session state | add a `SessionEventMap` member and render/replay from the log |
|
||||
| Manage a same-session objective | call `ctx.goals`; drive continuation through `Agent` and `agent/*` seams |
|
||||
| Fork a live session | use `ctx.sessions.fork(source, boundary?, childSessionId?)` |
|
||||
| Scope a tool, prompt section, or listener to ONE agent | register it through that agent's `agent.ctx` (see Agent Scope) |
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ flowchart LR
|
||||
svc_agents["ctx.agents<br/>Agent service"]
|
||||
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
|
||||
pkg_agent_spine_demo["agent-spine-demo"]
|
||||
pkg_goal["goal"]
|
||||
svc_goals["ctx.goals<br/>Same-session goal domain"]
|
||||
pkg_bash["bash"]
|
||||
svc_bash["ctx.bash<br/>Bash executor seam"]
|
||||
pkg_bash_local["bash-local"]
|
||||
@@ -109,6 +111,7 @@ flowchart LR
|
||||
pkg_compact_basic --> svc_compact
|
||||
pkg_fs --> svc_fs
|
||||
pkg_fs_local --> svc_fs
|
||||
pkg_goal --> svc_goals
|
||||
pkg_llm --> svc_llm
|
||||
pkg_llm_deepseek --> svc_llm
|
||||
pkg_llm_pi_ai --> svc_llm
|
||||
@@ -217,6 +220,7 @@ flowchart LR
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
|
||||
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
|
||||
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. |
|
||||
| `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. |
|
||||
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
|
||||
|
||||
@@ -318,6 +318,20 @@ export interface Config {
|
||||
|
||||
Source: [`packages/fs/fs-local/src/index.ts:38`](../packages/fs/fs-local/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-goal`
|
||||
|
||||
Requires: `agents`
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment defaults for goal creation. */
|
||||
export interface Config {
|
||||
/** Total rounds used when a create request omits its own cap. */
|
||||
defaultMaxGoalRounds?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/goal/goal/src/index.ts:56`](../packages/goal/goal/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-hooks-claude`
|
||||
|
||||
Requires: `bash`
|
||||
|
||||
@@ -450,6 +450,29 @@ Types: [FsTarget](../core-data-structures/filesystem.md) · [FsWriteIntent](../c
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:53`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
## `goal/*`
|
||||
|
||||
### `goal/changed` — emit
|
||||
|
||||
Goal mutation accepted by one live agent. The matching context event is already appended or queued in that agent's active tool-batch FIFO. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Goal mutation accepted by one live agent. The matching context event is
|
||||
* already appended or queued in that agent's active tool-batch FIFO.
|
||||
* Listener failures are contained.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @param agent - agent whose session owns the goal.
|
||||
* @param change - fresh current projection or clear tombstone.
|
||||
* @mode emit
|
||||
*/
|
||||
'goal/changed'(this: import('@deepseek-ai/dsh-scope').Scoped<Agent>, agent: Agent, change: GoalChanged): void
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
## `llm/*`
|
||||
|
||||
### `llm/stream` — waterfall
|
||||
|
||||
@@ -479,6 +479,106 @@ Types: [FsDirEntry](../core-data-structures/filesystem.md) · [FsEditOutcome](..
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:80`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
## `ctx.goals` — `GoalService`
|
||||
|
||||
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.
|
||||
* @returns a fresh view or `undefined` when no goal is current.
|
||||
* @throws {@link GoalError} when the agent is not the registry's live instance.
|
||||
*/
|
||||
get(agent: Agent): GoalView | undefined
|
||||
|
||||
/**
|
||||
* Create and arm a goal. A completed goal may be replaced; every other
|
||||
* current phase must be cleared or resumed instead.
|
||||
* @param agent - owning live agent.
|
||||
* @param request - objective and optional round cap.
|
||||
* @returns the created live view.
|
||||
*/
|
||||
create(agent: Agent, request: CreateGoalRequest): GoalView
|
||||
|
||||
/**
|
||||
* Edit objective and/or round cap without changing phase.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @param request - at least one replacement field.
|
||||
* @returns the edited view.
|
||||
*/
|
||||
edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView
|
||||
|
||||
/**
|
||||
* Pause an active goal and disarm automatic continuation.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the paused view.
|
||||
*/
|
||||
pause(agent: Agent, ref: GoalRef): GoalView
|
||||
|
||||
/**
|
||||
* Resume and arm a stopped goal, or rearm an active goal after a
|
||||
* session-start edge, while its round budget still has capacity.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the active view.
|
||||
*/
|
||||
resume(agent: Agent, ref: GoalRef): GoalView
|
||||
|
||||
/**
|
||||
* Mark a current non-complete goal complete and disarm it.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the completed view.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* Clear the current goal while retaining a durable tombstone and history.
|
||||
* @param agent - owning live agent.
|
||||
* @param ref - expected current revision.
|
||||
* @returns the tombstone ref whose revision is one past the cleared snapshot.
|
||||
*/
|
||||
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)
|
||||
|
||||
Source: [`packages/goal/goal/src/index.ts:97`](../../packages/goal/goal/src/index.ts)
|
||||
|
||||
## `ctx.llm` — `LlmService`
|
||||
|
||||
The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
|
||||
|
||||
@@ -18,6 +18,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
|
||||
| [llm-streaming.md](llm-streaming.md) | the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam |
|
||||
| [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions |
|
||||
| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
|
||||
| [goal.md](goal.md) | persisted goal identity, lifecycle snapshots, activation, change records, and round attribution |
|
||||
| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, the turn-enclosure invariant |
|
||||
| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` |
|
||||
| [session-query.md](session-query.md) | logical records, bounded exact-event reads, and relationship traces |
|
||||
|
||||
139
docs/core-data-structures/goal.md
Normal file
139
docs/core-data-structures/goal.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Same-session goals
|
||||
|
||||
Types shared by the event-sourced goal domain and its policy consumers. The [goal-domain RFC](../rfc/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) owns the persistence and activation decisions; this page records the literal shapes from [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts).
|
||||
|
||||
## Identity and lifecycle
|
||||
|
||||
`GoalId` is a [branded id](core.md#branded-ids). A caller mutates one exact revision through `GoalRef`; every accepted durable mutation increments the revision.
|
||||
|
||||
```ts type-equiv
|
||||
/** Compare-and-set identity for one exact goal revision. */
|
||||
interface GoalRef {
|
||||
/** Stable goal identity. */
|
||||
readonly id: GoalId
|
||||
/** Positive revision; every durable mutation increments it. */
|
||||
readonly revision: number
|
||||
}
|
||||
```
|
||||
|
||||
The durable phase answers what happened to the objective. Process-local activation separately answers whether a continuation consumer may start another round.
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable continuation phase. Activation is process-local and separate. */
|
||||
type GoalPhase =
|
||||
| 'active'
|
||||
| 'paused'
|
||||
| 'blocked'
|
||||
| 'usage-limited'
|
||||
| 'budget-limited'
|
||||
| 'complete'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Full durable state written by every non-clear goal mutation. */
|
||||
interface GoalSnapshot extends GoalRef {
|
||||
/** Human-requested completion objective. */
|
||||
readonly objective: string
|
||||
/** Durable lifecycle phase. */
|
||||
readonly phase: GoalPhase
|
||||
/** Total admitted goal-round cap. */
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Current goal projection, including values derived from the session log. */
|
||||
interface GoalView extends GoalSnapshot {
|
||||
/** Highest admitted round number for this goal. */
|
||||
readonly roundsStarted: number
|
||||
/** Epoch milliseconds of the create mutation. */
|
||||
readonly createdAt: number
|
||||
/** Epoch milliseconds of the latest mutation. */
|
||||
readonly updatedAt: number
|
||||
/** Process-local continuation eligibility; never persisted. */
|
||||
readonly activation: GoalActivation
|
||||
}
|
||||
```
|
||||
|
||||
## Durable changes
|
||||
|
||||
Every mutation is a `context/message` whose metadata is either a complete snapshot or a clear tombstone. The version, metadata, goal source, raw envelope, and rendered content form one replay invariant.
|
||||
|
||||
```ts type-equiv
|
||||
/** Full-snapshot goal mutation retained in a model-visible context event. */
|
||||
interface GoalSnapshotChangeMeta {
|
||||
readonly kind: 'goal/change'
|
||||
readonly version: 1
|
||||
readonly operation: Exclude<GoalOperation, 'clear'>
|
||||
readonly goal: GoalSnapshot
|
||||
readonly roundsStarted: number
|
||||
readonly createdAt: number
|
||||
readonly updatedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Tombstone retained when the current goal is cleared. */
|
||||
interface GoalClearChangeMeta {
|
||||
readonly kind: 'goal/change'
|
||||
readonly version: 1
|
||||
readonly operation: 'clear'
|
||||
readonly cleared: GoalRef
|
||||
readonly clearedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
Goal state changes use round `0`. A continuation consumer attributes each admitted user-message turn with a positive, sequential round number and the current revision; replay rejects gaps, stale revisions, stopped phases, and cap overflow.
|
||||
|
||||
```ts type-equiv
|
||||
/** Message attribution for durable goal state and continuation rounds. */
|
||||
interface GoalMessageSource {
|
||||
readonly kind: 'goal'
|
||||
readonly goalId: GoalId
|
||||
readonly revision: number
|
||||
/** Zero for state changes; positive for admitted continuation rounds. */
|
||||
readonly round: number
|
||||
}
|
||||
```
|
||||
|
||||
## 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`.
|
||||
|
||||
```ts type-equiv
|
||||
/** Input whose omitted round cap is resolved by the service configuration. */
|
||||
interface CreateGoalRequest {
|
||||
readonly objective: string
|
||||
readonly maxGoalRounds?: number
|
||||
}
|
||||
```
|
||||
|
||||
```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 {
|
||||
readonly objective?: string
|
||||
readonly maxGoalRounds?: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Live notification after one goal mutation has been accepted for logging. */
|
||||
interface GoalChanged {
|
||||
readonly operation: GoalOperation
|
||||
readonly ref: GoalRef
|
||||
/** Absent for a clear tombstone. */
|
||||
readonly goal?: GoalView
|
||||
}
|
||||
```
|
||||
|
||||
## Service behavior
|
||||
|
||||
[`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay, enforces exact-live-agent identity and compare-and-set mutations, overlays deferred injections, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) owns the callable and model-visible contract.
|
||||
@@ -18,7 +18,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:226`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:278`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:241`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:188`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:188`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:165`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:252`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:288`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
@@ -27,6 +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`) | - |
|
||||
| `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) |
|
||||
|
||||
@@ -15,3 +15,15 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
|
||||
- **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool surface for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one.
|
||||
- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/session-start` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
|
||||
- **lineage** — parent/child facts carried as data (`parentSession`, `subagentDepth`); never affects visibility. <a id="lineage"></a>
|
||||
|
||||
## 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 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.
|
||||
|
||||
## loop hierarchy
|
||||
|
||||
- **turn** — one drain of admitted input in a session, ending after the model and its tools stop or a terminal policy intervenes. <a id="turn"></a>
|
||||
- **step** — one model request plus the tool executions caused by its response; a turn contains one or more steps. <a id="step"></a>
|
||||
- **round** — an outer policy iteration containing a turn, such as a [goal round](#goal-round) or one fresh-agent Ralph attempt. Round counters belong to that policy and do not count every turn in a session. <a id="round"></a>
|
||||
|
||||
@@ -28,6 +28,9 @@ flowchart TD
|
||||
pkg_system_prompt["system-prompt"]
|
||||
pkg_tools["tools"]
|
||||
end
|
||||
subgraph group_goal["packages/goal"]
|
||||
pkg_goal["goal"]
|
||||
end
|
||||
subgraph group_bash["packages/bash"]
|
||||
pkg_bash["bash"]
|
||||
pkg_bash_local["bash-local"]
|
||||
@@ -194,6 +197,11 @@ flowchart TD
|
||||
pkg_llm_replay --> pkg_session
|
||||
pkg_sandbox_local --> pkg_llm
|
||||
pkg_sandbox_local --> pkg_sandbox
|
||||
pkg_goal --> pkg_agent
|
||||
pkg_goal --> pkg_brand
|
||||
pkg_goal --> pkg_llm
|
||||
pkg_goal --> pkg_scope
|
||||
pkg_goal --> pkg_session
|
||||
pkg_bash_local --> pkg_bash
|
||||
pkg_bash_local --> pkg_timeout
|
||||
pkg_compact_basic --> pkg_agent
|
||||
@@ -485,6 +493,7 @@ flowchart TD
|
||||
| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) |
|
||||
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
|
||||
| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) |
|
||||
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
|
||||
|
||||
@@ -89,6 +89,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
|
||||
| [Optional time-context plugin](implemented/feature/2026-07-14-time-context-plugin.md) | 2026-07-14 |
|
||||
| [Durable per-step time context](implemented/feature/2026-07-16-durable-per-step-time-context.md) | 2026-07-16 |
|
||||
| [Dedicated full-screen TUI front door](implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md) | 2026-07-17 |
|
||||
| [Persisted same-session goal domain](implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) | 2026-07-19 |
|
||||
|
||||
### Simplification
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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: ebf6168a4d8552d40e22db309f953f6f273511a9
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: 5fd35ee8faffc1c53d257bee48f36d70838fdee1
|
||||
@@ -0,0 +1,62 @@
|
||||
# RFC: Persisted same-session goal domain
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-19-persisted-same-session-goal-domain.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A long-running objective outlives one prompt, turn, or model request. Treating that objective as an in-memory loop variable loses it on process restart, while putting it only in UI state makes model behavior impossible to reconstruct. Treating every session turn as progress also charges unrelated human messages against an automatic-work budget.
|
||||
|
||||
Durable lifecycle and permission to continue are different facts. A session may retain an active objective after restart or fork, but silently starting work when a user opens that session is surprising. The domain needs replayable state without persisted auto-execution authority, and it must remain a plugin on the public agent/session seams rather than a special case in the concrete loop.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
When `Agent.inject()` defers a mutation inside an active tool batch, the service overlays the accepted payload in process memory so a later mutation can use its new revision. Reconciliation removes only an exact matching payload when the FIFO append becomes visible; the durable log remains authoritative after restart.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
### Service boundary
|
||||
|
||||
The service accepts only the exact live `Agent` object registered under its id. Successful mutation injection emits the scoped `goal/changed` event with contained listener failures. Policy consumers use this service plus the public `Agent` interface and `agent/*` events; the goal domain does not import or modify `dsh-agent-loop`.
|
||||
|
||||
## 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, 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
|
||||
|
||||
- **Store goals in a separate database or session header** — rejected because the session log already supplies ordering, persistence, fork prefixes, and reconstructability; a second store introduces atomicity and lineage questions.
|
||||
- **Use hidden log-only events** — rejected because durable state that changes future model behavior must be model-visible and reconstructable under the repository's logging invariant.
|
||||
- **Persist activation and restart automatically** — rejected because opening or resuming a session must wait for human input; durable phase records status, not fresh authority to spend resources.
|
||||
- **Count all session turns as goal rounds** — rejected because one session can contain human clarification, inspection, and unrelated work; only goal-attributed continuation turns consume this budget.
|
||||
- **Add goal state or a generic loop abstraction to `dsh-agent-loop`** — rejected because state and continuation policy can compose through existing plugins, `Agent` verbs, and events without privileging the shipped loop implementation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Goal history survives persistence, resume, compaction of unrelated nodes, and session fork as ordinary session data.
|
||||
- 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.
|
||||
|
||||
## Known limitations and deferred work
|
||||
|
||||
- This domain records state but does not schedule goal rounds, cancel active turns, or classify abnormal stops.
|
||||
- The actor that records `complete` or `blocked` is authoritative; an independent evaluator or completion certificate is deferred to a policy consumer.
|
||||
- There is one current goal per session; parallel objective graphs and cross-session goal storage are absent.
|
||||
- `GOAL_CHANGE_VERSION` has no pre-release compatibility promise or migration path.
|
||||
@@ -0,0 +1,62 @@
|
||||
# RFC: 持久的同会话目标领域
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-19-persisted-same-session-goal-domain.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
长时间运行的目标会跨越单个提示词、轮次或模型请求。若把该目标视为内存中的循环变量,进程重启时就会丢失;若只存放在 UI 状态中,又无法重建模型行为。若把会话中的每个轮次都视为目标进度,与自动工作无关的人类消息也会消耗预算。
|
||||
|
||||
持久生命周期与继续执行的权限是两个不同事实。会话在重启或 fork(派生)后可以保留活跃目标,但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态,却不能持久化自动执行权限;它还必须作为公共 agent(智能体)与会话接缝上的插件存在,而不是具体循环中的特例。
|
||||
|
||||
## 决策
|
||||
|
||||
位于 `packages/goal/goal/` 的 `@deepseek-ai/dsh-goal` 通过 `ctx.goals` 管理一个当前的同会话目标。目标包含品牌化 id、目标描述、持久阶段、比较并交换修订号和 `maxGoalRounds`。`defaultMaxGoalRounds` 是经过校验的部署配置,默认值为 `256`;`resolveCreate()` 在变更前将其解析为完整值。
|
||||
|
||||
持久阶段包括 `active`、`paused`、`blocked`、`usage-limited`、`budget-limited` 和 `complete`。独立的实时激活态为 `armed` 或 `disarmed`。创建与显式恢复会激活目标;暂停、完成、阻塞、达到限制和清除都会解除激活。编辑保留激活态。持久快照绝不包含激活态。
|
||||
|
||||
### 持久记录与回放
|
||||
|
||||
每次非清除变更都通过 `Agent.inject()` 追加一条原始且模型可见的 `context/message`,其中包含带版本的完整快照。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`;元数据必须与渲染后的 `<goal_state>...</goal_state>` 内容完全一致。会话日志是唯一的持久事实来源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。
|
||||
|
||||
回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
|
||||
|
||||
当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内叠加已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重启后仍以持久日志为准。
|
||||
|
||||
### 生命周期与实时激活态
|
||||
|
||||
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`,陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时,停止阶段或已解除激活的活跃目标才能恢复;只有已接纳回合数达到上限后,才能标记预算受限。
|
||||
|
||||
从任何种子构建的缓存都以未激活状态开始,每次 `agent/session-start` 边沿也会再次解除激活。因此,恢复和 fork 会保留持久目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略表面可以显式调用恢复操作并激活目标。
|
||||
|
||||
### 服务边界
|
||||
|
||||
服务只接受在对应 id 下注册的同一个实时 `Agent` 对象。成功注入变更后,它会发出带作用域的 `goal/changed` 事件,并隔离监听器失败。策略消费者通过本服务、公共 `Agent` 接口和 `agent/*` 事件工作;目标领域既不导入也不修改 `dsh-agent-loop`。
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动时解除激活与活跃目标重新激活、FIFO 延迟变更协调、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **把目标存入独立数据库或会话头**——不予采纳,因为会话日志已经提供顺序、持久化、fork 前缀与可重建性;第二份存储会引入原子性和谱系问题。
|
||||
- **使用模型不可见的纯日志事件**——不予采纳,因为会改变后续模型行为的持久状态必须满足仓库日志不变量,保持模型可见且可重建。
|
||||
- **持久化激活态并自动重启**——不予采纳,因为打开或恢复会话时必须等待人类输入;持久阶段记录状态,而不是再次消耗资源的授权。
|
||||
- **把所有会话轮次都计为目标回合**——不予采纳,因为同一会话可以包含人类澄清、检查和无关工作;只有归属于目标的继续执行轮次才消耗该预算。
|
||||
- **向 `dsh-agent-loop` 添加目标状态或通用循环抽象**——不予采纳,因为状态与继续执行策略可以通过现有插件、`Agent` 动词和事件组合,而无需赋予默认循环实现特权。
|
||||
|
||||
## 后果
|
||||
|
||||
- 目标历史作为普通会话数据,在持久化、恢复、无关节点压缩和会话 fork 后继续保留。
|
||||
- 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
|
||||
- 完整快照便于检查和严格回放,但在压缩隐藏它们之前,会在模型历史中重复目标描述与状态字段。
|
||||
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
|
||||
- 回合上限只约束继续执行次数;token、费用、时间和提供方限制仍属于独立策略。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- 本领域记录状态,但不调度目标回合、不取消活跃轮次,也不分类异常停止。
|
||||
- 记录 `complete` 或 `blocked` 的参与者具有最终权威;独立评估器或完成证书延期到策略消费者中实现。
|
||||
- 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
|
||||
- `GOAL_CHANGE_VERSION` 在首次发布前不承诺兼容性,也不提供迁移路径。
|
||||
Reference in New Issue
Block a user