Merge origin/master into worktree/explicit-turn-signal

This commit is contained in:
Yichen Jiang
2026-07-20 21:38:49 +08:00
1322 changed files with 47895 additions and 20257 deletions
+25 -34
View File
@@ -8,11 +8,11 @@
import type { Context } from 'cordis'
import { agentEvents, normalizeAgentCancelCause } from '@deepseek-ai/dsh-agent'
import type { AgentCancelCause, AgentId, AgentOptions, AgentStatus, HookContext, InjectOptions, SendOptions } from '@deepseek-ai/dsh-agent'
import type { AgentCancelCause, AgentOptions, AgentStatus, HookContext, InjectOptions, SendOptions } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { deepFreeze } from '@deepseek-ai/dsh-llm'
import { deepFreeze, errorChain } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import { snapshotJsonValue, type Session } from '@deepseek-ai/dsh-session'
import { snapshotJsonValue, type Session, type SessionId } from '@deepseek-ai/dsh-session'
import { DISPOSED_INTERRUPT_REASON, TurnCancellation } from './cancellation.ts'
import { Inbox, type InboxMessage } from './inbox.ts'
import { isTurnOpen, lastTurnNumber, runLoop } from './loop.ts'
@@ -59,7 +59,11 @@ export interface PreparedReactLoopAgent {
* @returns the agent and closures bound only to that exact instance.
*/
export function prepareReactLoopAgent(
ctx: Context, id: AgentId, options: AgentOptions, session: Session, maxParallelToolCalls: number,
ctx: Context,
id: SessionId,
options: AgentOptions,
session: Session,
maxParallelToolCalls: number,
): PreparedReactLoopAgent {
if (claimedDriverSessions.has(session)) {
throw new Error(`session "${session.id}" already has a concrete agent driver`)
@@ -77,7 +81,6 @@ export function prepareReactLoopAgent(
},
}
}
/**
* Install the concrete agent's scope context exactly once. Construction and
* scope minting are mutually referential (the scope key is the agent), so the
@@ -93,7 +96,7 @@ export function bindReactLoopAgentContext(agent: ReactLoopAgent, ctx: Context):
/**
* The concrete {@link Agent} implementation owned by the agent-loop plugin.
*
* Owns the inbox (queued + steering FIFOs), one turn cancellation holder, and
* Owns the inbox (queued + steering FIFOs), turn cancellation, and
* the loop driver. Everything observable happens through session events and
* the agent/* event taxonomy — plugins never need this class.
*/
@@ -118,17 +121,13 @@ export class ReactLoopAgent implements Agent {
}
private _status: AgentStatus = 'idle'
/** Active turn owner, installed before the running notification and retained through flush. */
/** Active turn owner from pre-running publication through durability settlement. */
private turnCancellation: TurnCancellation | undefined
/** Whether runLoop has been installed into {@link done}. */
private driverStarted = false
/** Whether registry publication began and status disposal is externally visible. */
private published = false
/**
* Cause-less marker for queued work cancelled before the driver installs a
* turn owner. It never represents an active turn and cannot leak a cause into
* replacement work.
*/
/** Cause-less marker for queued work cancelled before the driver installs a turn owner. */
private preRunCancelled = false
private disposed: Promise<void>
private resolveDisposed!: () => void
@@ -157,7 +156,7 @@ export class ReactLoopAgent implements Agent {
constructor(
private loopCtx: Context,
public readonly id: AgentId,
public readonly id: SessionId,
public readonly options: AgentOptions,
public readonly session: Session,
maxParallelToolCalls: number,
@@ -246,7 +245,6 @@ export class ReactLoopAgent implements Agent {
const context = {
content,
source,
...options?.envelope !== undefined ? { envelope: options.envelope } : {},
...options?.meta !== undefined ? { meta: options.meta } : {},
}
if (isTurnOpen(this.session)) {
@@ -285,7 +283,7 @@ export class ReactLoopAgent implements Agent {
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) => {
const rendered = renderThrown(error)
const rendered = errorChain(error)
const err = error instanceof Error ? error : new Error(rendered)
this.loopCtx.logger.warn(`agent "${this.id}": flush after idle injection failed: ${rendered}`)
agentEvents(this.loopCtx, this).emit('agent/error', turn, 0, err)
@@ -327,18 +325,16 @@ export class ReactLoopAgent implements Agent {
}
cancel(cause?: AgentCancelCause): void {
// Validate before the idle no-op so misuse fails consistently in every state.
const accepted = normalizeAgentCancelCause(cause ?? { kind: 'user' })
const active = this.turnCancellation
if (active === undefined && !this.#inbox.hasQueued && !this.#inbox.hasSteering) return
if (active === undefined) this.preRunCancelled = true
// Drop all pending queued + steering work (un-started prompts never run; the
// cancelled turn's steering is not re-enqueued). Clear before abort dispatch,
// whose synchronous observers may enqueue replacement work that must survive.
// This is direct even when the loop is parked in waitForQueued — there is no
// turn to stop and nothing left for the parked loop to run, so no wake is needed.
const normalized = normalizeAgentCancelCause(cause ?? { kind: 'user' })
const cancellation = this.turnCancellation
const preRun = cancellation === undefined && (this.#inbox.hasQueued || this.#inbox.hasSteering)
if (preRun) {
this.preRunCancelled = true
}
// Clear work already present before abort observers run. A replacement
// synchronously enqueued by an observer belongs to the next turn.
this.#inbox.clear()
if (active !== undefined) active.request(accepted)
cancellation?.request(normalized)
}
/**
@@ -376,7 +372,7 @@ export class ReactLoopAgent implements Agent {
[startDriver](): void {
if (this._status === 'disposed') return
this.driverStarted = true
this.done = this.loopCtx.agentExecution.run({ agent: this }, () => runLoop(this.loopCtx, this, {
this.done = this.loopCtx.agents.withInitiator(this, () => runLoop(this.loopCtx, {
inbox: this.#inbox,
maxParallelToolCalls: this.maxParallelToolCalls,
setStatus: (status) => { this.setStatus(status) },
@@ -386,7 +382,7 @@ export class ReactLoopAgent implements Agent {
return cancellation
},
clearTurnCancellation: (cancellation) => {
/* v8 ignore else -- the internal driver clears only the exact holder returned by its latest install */
/* v8 ignore else -- the driver clears only the exact owner returned by its latest install. */
if (this.turnCancellation === cancellation) this.turnCancellation = undefined
},
disposed: this.disposed,
@@ -394,7 +390,7 @@ export class ReactLoopAgent implements Agent {
isPreRunCancelled: () => this.preRunCancelled,
clearPreRunCancel: () => { this.preRunCancelled = false },
withToolBatch: run => this.withToolBatch(run),
// Pre-run cancellation re-parks without emitting a status transition.
// Pre-run cancellation settles queued-work waiters before publishing idle.
settleIdle: () => { this.settleIdleWaiters() },
}))
}
@@ -441,8 +437,3 @@ export class ReactLoopAgent implements Agent {
}
}
}
/** Render an ordinary thrown value for the error event and log. */
function renderThrown(value: unknown): string {
return value instanceof Error ? value.message : String(value)
}