Merge remote-tracking branch 'origin/master' into codex/status-bar-token-metrics

Conflict resolution notes:

- StatsLine: master redesigned the row into pipe-separated groups with LLM and
  tool wall times. Kept that design and swapped only the token accounting
  source, so counts and durations stay window-scoped while billing and context
  occupancy read the durable projections.
- Generated artifacts (cordis catalog, module graph, event producer/consumer,
  i18n pairing hashes) and web snapshots took master's side; they are
  regenerated and re-recorded after this merge.
- Web e2e goldens and details-panel/timeline assertions took master's side:
  that evolution is unrelated to this branch.
- ui-conversation package.json: kept master's devDependency ordering, re-adding
  only the token-meter entry this branch needs.
This commit is contained in:
Hypatia May
2026-07-30 13:56:39 +08:00
1091 changed files with 45715 additions and 7873 deletions

View File

@@ -8,13 +8,18 @@
*/
import type { Context } from 'cordis'
import { agentCarrier, assembleContextFor, emitAgentEvent } from '@deepseek-ai/dsh-agent'
import { randomUUID } from 'node:crypto'
import { agentCarrier, assembleContextFor, emitAgentEvent, InboxItemId } from '@deepseek-ai/dsh-agent'
import { createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import type {
Agent,
CancelOptions,
AgentInterruptReason,
InboxAction,
InboxActionResult,
InboxItem,
InboxItemId as InboxItemIdType,
InboxPlacement,
AgentOptions,
AgentStatus,
@@ -55,9 +60,9 @@ type StepOutcome =
*/
export class ReactLoopAgent implements Agent {
/** Prompts awaiting individual turns. */
private queued: { message: UserMessage; wakeup: boolean }[] = []
private queued: { item: InboxItem; wakeup: boolean }[] = []
/** Input taken into the session log at step boundaries. */
private outbox: { message: UserMessage; steering: boolean }[] = []
private outbox: { message: UserMessage; steering: boolean; item?: InboxItem }[] = []
/** Whether observers see a running interval; consecutive turns share it. */
private busy = false
@@ -115,16 +120,52 @@ export class ReactLoopAgent implements Agent {
}
const placement: InboxPlacement = target === 'next-step' && this.acceptsNextStep ? 'steering' : 'queued'
const item: InboxItem = Object.freeze({
id: InboxItemId(randomUUID()),
message,
placement,
})
if (placement === 'steering') {
this.outbox.push({ message, steering: true })
this.outbox.push({ message, steering: true, item })
} else {
this.queued.push({ message, wakeup })
this.queued.push({ item, wakeup })
}
// Preserve the routing decision for every send in this synchronous caller
// stack, while installing quiescence ownership before enqueue observers
// can cancel or dispose.
if (placement === 'queued' && wakeup) this.scheduleKick()
emitAgentEvent(this.loopCtx, this, 'agent/inbox/enqueue', message, placement)
emitAgentEvent(this.loopCtx, this, 'agent/inbox/enqueue', item)
}
/** Apply one synchronous mutation to a still-pending queued occurrence. */
updateInbox(id: InboxItemIdType, action: InboxAction): InboxActionResult {
const queuedIndex = this.queued.findIndex(candidate => candidate.item.id === id)
if (queuedIndex === -1) return 'not-found'
const pending = this.queued[queuedIndex]
/* v8 ignore next -- the index was resolved from this array without an async boundary. */
if (pending === undefined) throw new Error(`agent "${this.id}" queued item disappeared during update`)
/* v8 ignore next -- InboxAction is a closed discriminated union; all variants are covered below. */
switch (action.kind) {
case 'edit': {
const item: InboxItem = Object.freeze({
...pending.item,
message: freezeMessage({ ...pending.item.message, content: action.content }),
})
this.queued[queuedIndex] = { ...pending, item }
emitAgentEvent(this.loopCtx, this, 'agent/inbox/update', item)
return 'applied'
}
case 'remove': {
this.queued.splice(queuedIndex, 1)
emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', [pending.item])
return 'applied'
}
default:
/* v8 ignore next -- InboxAction is a closed discriminated union. */
return assertNever(action)
}
}
/** Queue one ordinary prompt turn and wake the driver. */
@@ -169,9 +210,9 @@ export class ReactLoopAgent implements Agent {
if (cause.kind !== 'disposed') emitAgentEvent(this.loopCtx, this, 'agent/cancel-requested', cause)
}
if (!options.keepInbox) {
const discarded = this.queued.map(item => item.message)
const discarded = this.queued.map(item => item.item)
for (const item of this.outbox) {
if (item.steering) discarded.push(item.message)
if (item.steering && item.item !== undefined) discarded.push(item.item)
}
// Clear before abort observers run: replacement work belongs to the next turn.
this.queued.length = 0
@@ -221,8 +262,9 @@ export class ReactLoopAgent implements Agent {
if (this.abort !== undefined || !this.queued.some(item => item.wakeup)) return
// The some() guard above proves the queue is non-empty; the non-null
// assertion expresses that invariant.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { message } = this.queued.shift()!
// oxlint-disable-next-line typescript/no-non-null-assertion
const { item } = this.queued.shift()!
const { message } = item
const inheritedOutboxLength = this.outbox.length
const admission = new AbortController()
@@ -293,7 +335,7 @@ export class ReactLoopAgent implements Agent {
// Published only after the abort owner and pending done are installed: a
// dequeue listener that cancels or disposes must find live cancellation
// and quiescence ownership, not the previous activity's settled state.
emitAgentEvent(this.loopCtx, this, 'agent/inbox/dequeue', message, 'queued')
emitAgentEvent(this.loopCtx, this, 'agent/inbox/dequeue', item)
}
/**
@@ -368,7 +410,7 @@ export class ReactLoopAgent implements Agent {
outcome.failure, requestFailureHistory, outcome.retryPolicy, signal,
() => Promise.resolve<RequestErrorAction>(undefined),
)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited.
// oxlint-disable-next-line typescript/no-unnecessary-condition -- signal can abort while recovery is awaited.
if (action?.kind === 'retry' && !signal.aborted) {
retryFailures = Object.freeze([...requestFailureHistory, outcome.failure])
}
@@ -584,7 +626,7 @@ export class ReactLoopAgent implements Agent {
const maxTokens = this.options.maxTokens
const seedConfig = deepFreeze(structuredClone(
this.requestHeaderLogged
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the instance logged the header it now folds
// oxlint-disable-next-line typescript/no-non-null-assertion -- the instance logged the header it now folds
? persistedConfig!
: {
...route,
@@ -660,7 +702,9 @@ export class ReactLoopAgent implements Agent {
for (const item of this.outbox.splice(0, limit)) {
if (item.steering) {
steered = true
emitAgentEvent(this.loopCtx, this, 'agent/inbox/dequeue', item.message, 'steering')
/* v8 ignore next -- only inbox-backed steer entries carry steering:true. */
if (item.item === undefined) throw new Error(`agent "${this.id}" steering outbox item has no inbox identity`)
emitAgentEvent(this.loopCtx, this, 'agent/inbox/dequeue', item.item)
this.session.append(
'steering/message',
{ turn, message: item.message },