docs: rebalance prose cleanup and add trimming skill

This commit is contained in:
Tianyi Cui
2026-07-13 23:27:00 +08:00
parent fcdc318dda
commit 148046b9c8
392 changed files with 2801 additions and 1754 deletions

View File

@@ -48,7 +48,8 @@ export interface PreparedReactLoopAgent {
}
/**
* Construct an unpublished concrete agent with instance-bound lifecycle controls.
* Construct an unpublished concrete agent with instance-bound lifecycle
* controls. Only those paired controls can publish or start this instance.
* @param ctx - the agent-loop service context used for driving and events.
* @param id - the concrete agent identity.
* @param options - loop options for the agent.
@@ -253,7 +254,8 @@ export class ReactLoopAgent implements Agent {
// Decide the durability checkpoint from the log: an accepted one-shot
// turn must be flushed even when its message append was the failing step.
const turnRecorded = this.session.events.some(e => e.type === 'turn/start' && e.data.turn === turn)
// Track the asynchronous checkpoint so disposal drains it; contain errors.
// Keep inject() synchronous: report checkpoint failures live instead of
// rejecting the caller, and track the task so disposal still drains it.
if (turnRecorded) {
// Through the store's flush (the carrier owner), never a raw parallel.
const flush = this.loopCtx.sessions.flush(this.session).catch((error: unknown) => {
@@ -290,7 +292,11 @@ export class ReactLoopAgent implements Agent {
this.currentAbort?.abort(reason ?? 'cancelled')
}
/** Resolve at idle, or after driver exit when disposed. */
/**
* Resolve immediately when idle with no queued work, on the next quiescent
* idle transition otherwise, or after driver exit when already disposed.
* This observes quiescence; it does not own teardown.
*/
whenIdle(): Promise<void> {
if (this._status === 'disposed') return this.done
if (this._status !== 'running' && !this.#inbox.hasQueued) return Promise.resolve()
@@ -330,7 +336,7 @@ export class ReactLoopAgent implements Agent {
isCancelled: () => this.cancelRequested,
cancelReason: () => this.cancelReason,
clearCancel: () => { this.cancelRequested = false },
// Pre-step cancellation re-parks without a status transition.
// Pre-step cancellation re-parks without emitting a status transition.
settleIdle: () => { this.settleIdleWaiters() },
})
}
@@ -370,7 +376,8 @@ export class ReactLoopAgent implements Agent {
// cleanup. The normal loop contains turn failures itself; allSettled is the
// final lifecycle backstop for anything outside those boundaries.
await Promise.allSettled([this.done])
// Repeat because settled flushes retire in adjacent promise reactions.
// Repeat because settled flushes retire in adjacent promise reactions;
// allSettled keeps reporting failures from skipping ownership teardown.
while (this.pendingIdleFlushes.size > 0) {
await Promise.allSettled([...this.pendingIdleFlushes])
}

View File

@@ -73,7 +73,11 @@ function signalAbortError(id: AgentId, signal: AbortSignal): Error {
return new Error(`agent "${id}" creation aborted`, { cause: signal.reason })
}
/** Caller-owned create/resume transaction through publication and teardown. */
/**
* Caller-owned create/resume transaction through rollback-covered publication
* and quiescent teardown. Resources remain private until the final registry
* entry arbitrates identity.
*/
class AgentCreationTransaction {
private active = true
private failure: Error | undefined

View File

@@ -1,4 +1,9 @@
/** Agent loop driver with turn-level error containment. @module dsh-agent-loop/loop */
/**
* Drives one agent across queued durable turns. Turn failures are contained so
* later work can run; the session log, not this driver, owns conversation state.
* See docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md.
* @module dsh-agent-loop/loop
*/
import type { Context } from 'cordis'
import type { FinishReason, GenerateOptions, LlmCallConfig, Message } from '@deepseek-ai/dsh-llm'
@@ -78,7 +83,7 @@ export interface LoopHandle {
cancelReason(): string
/** Clear the cancel marker (called once per iteration after the turn returns). */
clearCancel(): void
/** Settle idle waiters when a cancelled turn is skipped without a status transition. */
/** Settle idle waiters when pre-running cancellation skips a turn, without emitting `agent/status`. */
settleIdle(): void
}
@@ -267,7 +272,9 @@ async function runTurn(
break
}
// Compose, detach, and freeze the per-instance prefix before pressure checks.
// Compose the request-only prefix once per loop instance before pressure
// checks. It precedes all derived history and is recorded only in the
// request header, not as session history.
if (transmission.sessionPrefix === undefined) {
const emptyPrefix: Message[] = deepFreeze([])
const composed = await events.waterfall(
@@ -294,7 +301,8 @@ async function runTurn(
break
}
// Snapshot the exact log prefix before step/start: the reconstruction boundary.
// Snapshot the exact log prefix before step/start: the reconstruction
// boundary. Appends after this synchronous snapshot join the next request.
const boundaryMessages = session.deriveMessages()
session.append('step/start', { turn, step })
@@ -444,13 +452,12 @@ function drainSteering(agent: ReactLoopAgent, inbox: Inbox, turn: number): boole
return messages.length > 0
}
/** One step: build the request from the boundary snapshot + the step's
* header → compose the session prefix if this instance has none yet → log
* the header event the request owes → stream model → record → execute
* tools. The caller assembles the
* system prompt, fires the `agent/pre-step` seam, snapshots the derivation,
* and opens the step BEFORE calling this, so `boundaryMessages` is exactly
* the surface prefix at step/start and already reflects any compaction. */
/**
* Run one committed step: transform call config, log the request header, build
* the request from the cached prefix plus the step-boundary snapshot, stream and
* record the response, then execute tools. The caller has already assembled the
* prompt, run `agent/pre-step`, snapshotted history, and opened the step.
*/
async function runStep(
ctx: Context,
events: AgentEventDispatch,
@@ -560,7 +567,8 @@ async function runStep(
} catch {
parsedArguments = call.arguments
}
// TODO(pre-tool-input-rewrite): A rewrite must keep logged history and live presentation aligned.
// TODO(pre-tool-input-rewrite): Keep logged history and live presentation aligned;
// see docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md.
const result = await ctx.tools.execute({
callId: call.id,
name: call.name,

View File

@@ -1,7 +1,7 @@
/**
* Per-loop-instance transmission bookkeeping for the reconstructability contract: which header
* event to append before a request so the session log always explains the request (the
* reconstructability RFC).
* Per-loop-instance request-header bookkeeping for reconstructability. The
* comparison baseline is the header folded from the session log, so a fresh
* loop instance needs no special resume or fork state.
* @module dsh-agent-loop/request-log
*/
@@ -32,8 +32,10 @@ export function createTransmissionLog(): TransmissionLog {
}
/**
* Append whatever header event this request owes the log, so folding the log reproduces the
* header the request was built under. Exactly one of four things happens.
* Append whatever header event makes the log reproduce this request's header.
* The first request from an instance always records a full `initial` or `resume`
* snapshot. Later requests record nothing when unchanged, a round-tripping
* delta when expressible, or a full `fallback` snapshot otherwise.
*
* @param session - the session whose log explains the request.
* @param state - this loop instance's bookkeeping (mutated on first log).