|
|
|
|
@@ -29,6 +29,8 @@ import type {
|
|
|
|
|
RequestError,
|
|
|
|
|
RequestErrorAction,
|
|
|
|
|
SendOptions,
|
|
|
|
|
SteeringOutcome,
|
|
|
|
|
SteeringReceipt,
|
|
|
|
|
} from '@deepseek-ai/dsh-agent'
|
|
|
|
|
import {
|
|
|
|
|
BlockAssembler,
|
|
|
|
|
@@ -56,6 +58,26 @@ type StepOutcome =
|
|
|
|
|
| { kind: 'completed'; continueTurn: boolean; concluded: boolean; maxTokens: boolean }
|
|
|
|
|
| { kind: 'request-failed'; error: RequestError; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined }
|
|
|
|
|
|
|
|
|
|
/** Internal one-shot controller paired with a public steering receipt. */
|
|
|
|
|
interface SteeringDelivery {
|
|
|
|
|
readonly receipt: SteeringReceipt
|
|
|
|
|
settle(outcome: SteeringOutcome): void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Create one idempotent steering-admission controller. */
|
|
|
|
|
function createSteeringDelivery(): SteeringDelivery {
|
|
|
|
|
const { promise, resolve } = Promise.withResolvers<SteeringOutcome>()
|
|
|
|
|
let settled = false
|
|
|
|
|
return {
|
|
|
|
|
receipt: { outcome: promise },
|
|
|
|
|
settle(outcome): void {
|
|
|
|
|
if (settled) return
|
|
|
|
|
settled = true
|
|
|
|
|
resolve(outcome)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const RUNTIME_CONTEXT_SOURCE = '@deepseek-ai/dsh-system-prompt'
|
|
|
|
|
/** Clearing marker kept distinct from every prefixed {@link renderContextSnapshot} result. */
|
|
|
|
|
const CLEARED_RUNTIME_CONTEXT = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
|
|
|
|
|
@@ -112,9 +134,13 @@ function requestProposal(header: EpochHeader): LlmCallConfig {
|
|
|
|
|
*/
|
|
|
|
|
export class ReactLoopAgent implements Agent {
|
|
|
|
|
/** Prompts awaiting individual turns. */
|
|
|
|
|
private queued: { item: InboxItem; wakeup: boolean }[] = []
|
|
|
|
|
private queued: { item: InboxItem; wakeup: boolean; delivery?: SteeringDelivery }[] = []
|
|
|
|
|
/** Input taken into the session log at step boundaries. */
|
|
|
|
|
private outbox: { message: UserMessage; steering: boolean; item?: InboxItem }[] = []
|
|
|
|
|
private outbox: { message: UserMessage; steering: boolean; item?: InboxItem; delivery?: SteeringDelivery }[] = []
|
|
|
|
|
/** Steering already committed to the log but not yet captured by a request. */
|
|
|
|
|
private pendingAdmissions: SteeringDelivery[] = []
|
|
|
|
|
/** Whether the active cancellation preserves already committed pending delivery. */
|
|
|
|
|
private preservePendingAdmissionsOnAbort = false
|
|
|
|
|
|
|
|
|
|
/** Whether observers see a running interval; consecutive turns share it. */
|
|
|
|
|
private busy = false
|
|
|
|
|
@@ -142,8 +168,6 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
/** Whether the session log is owed a matching turn end event. */
|
|
|
|
|
private turnOpen = false
|
|
|
|
|
private stepOpen = false
|
|
|
|
|
/** Whether {@link trySteer} can still join the current step's final drain. */
|
|
|
|
|
private strictSteeringOpen = false
|
|
|
|
|
/** Whether this loop instance has appended its initial/resume request anchor. */
|
|
|
|
|
private requestHeaderLogged = false
|
|
|
|
|
|
|
|
|
|
@@ -167,6 +191,15 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
send(
|
|
|
|
|
message: UserMessage,
|
|
|
|
|
options: SendOptions,
|
|
|
|
|
): void {
|
|
|
|
|
this.route(message, options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Route one accepted message, optionally tracking steering admission. */
|
|
|
|
|
private route(
|
|
|
|
|
message: UserMessage,
|
|
|
|
|
options: SendOptions,
|
|
|
|
|
delivery?: SteeringDelivery,
|
|
|
|
|
): void {
|
|
|
|
|
const { target, wakeup } = options
|
|
|
|
|
if (target === 'next-step' && !wakeup) {
|
|
|
|
|
@@ -185,9 +218,9 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
placement,
|
|
|
|
|
})
|
|
|
|
|
if (placement === 'steering') {
|
|
|
|
|
this.outbox.push({ message, steering: true, item })
|
|
|
|
|
this.outbox.push({ message, steering: true, item, ...delivery === undefined ? {} : { delivery } })
|
|
|
|
|
} else {
|
|
|
|
|
this.queued.push({ item, wakeup })
|
|
|
|
|
this.queued.push({ item, wakeup, ...delivery === undefined ? {} : { delivery } })
|
|
|
|
|
}
|
|
|
|
|
// Preserve the routing decision for every send in this synchronous caller
|
|
|
|
|
// stack, while installing quiescence ownership before enqueue observers
|
|
|
|
|
@@ -218,6 +251,7 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
}
|
|
|
|
|
case 'remove': {
|
|
|
|
|
this.queued.splice(queuedIndex, 1)
|
|
|
|
|
pending.delivery?.settle({ status: 'rejected' })
|
|
|
|
|
emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', [pending.item])
|
|
|
|
|
return 'applied'
|
|
|
|
|
}
|
|
|
|
|
@@ -235,22 +269,14 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Steer the open turn, falling back to a waking prompt while idle. */
|
|
|
|
|
steer(input: UserMessage): void {
|
|
|
|
|
this.send(input, {
|
|
|
|
|
/** Steer the open turn, falling back to a tracked waking prompt while idle. */
|
|
|
|
|
steer(input: UserMessage): SteeringReceipt {
|
|
|
|
|
const delivery = createSteeringDelivery()
|
|
|
|
|
this.route(input, {
|
|
|
|
|
target: 'next-step',
|
|
|
|
|
wakeup: true,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Atomically steer only while the current step still owns its final drain. */
|
|
|
|
|
trySteer(input: UserMessage): boolean {
|
|
|
|
|
if (!this.strictSteeringOpen) return false
|
|
|
|
|
this.send(input, {
|
|
|
|
|
target: 'next-step',
|
|
|
|
|
wakeup: true,
|
|
|
|
|
})
|
|
|
|
|
return true
|
|
|
|
|
}, delivery)
|
|
|
|
|
return delivery.receipt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Append model-facing context without waking the driver. */
|
|
|
|
|
@@ -304,11 +330,17 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
// inboxes clear; listener failures are contained by the dispatcher.
|
|
|
|
|
if (cause.kind !== 'disposed') emitAgentEvent(this.loopCtx, this, 'agent/cancel-requested', cause)
|
|
|
|
|
}
|
|
|
|
|
if (options.keepInbox && this.abort !== undefined) this.preservePendingAdmissionsOnAbort = true
|
|
|
|
|
if (!options.keepInbox) {
|
|
|
|
|
const discarded = this.queued.map(item => item.item)
|
|
|
|
|
for (const item of this.queued) item.delivery?.settle({ status: 'rejected' })
|
|
|
|
|
for (const item of this.outbox) {
|
|
|
|
|
if (item.steering && item.item !== undefined) discarded.push(item.item)
|
|
|
|
|
if (item.steering && item.item !== undefined) {
|
|
|
|
|
item.delivery?.settle({ status: 'rejected' })
|
|
|
|
|
discarded.push(item.item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.rejectPendingAdmissions()
|
|
|
|
|
// Clear before abort observers run: replacement work belongs to the next turn.
|
|
|
|
|
this.queued.length = 0
|
|
|
|
|
this.outbox.length = 0
|
|
|
|
|
@@ -373,7 +405,8 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
// The some() guard above proves the queue is non-empty; the non-null
|
|
|
|
|
// assertion expresses that invariant.
|
|
|
|
|
// oxlint-disable-next-line typescript/no-non-null-assertion
|
|
|
|
|
const { item } = this.queued.shift()!
|
|
|
|
|
const pending = this.queued.shift()!
|
|
|
|
|
const { item, delivery } = pending
|
|
|
|
|
const { message } = item
|
|
|
|
|
const inheritedOutboxLength = this.outbox.length
|
|
|
|
|
|
|
|
|
|
@@ -423,6 +456,7 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
// still owns the slot here and releasing it unconditionally is exact.
|
|
|
|
|
this.abort = undefined
|
|
|
|
|
if (admitted === undefined) {
|
|
|
|
|
delivery?.settle({ status: 'rejected' })
|
|
|
|
|
this.acceptsNextStep = false
|
|
|
|
|
try {
|
|
|
|
|
this.flushRejectedAdmissionContexts()
|
|
|
|
|
@@ -440,7 +474,7 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
this.continueOrIdle()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
await this.run(trigger, admitted, inheritedOutboxLength)
|
|
|
|
|
await this.run(trigger, admitted, inheritedOutboxLength, Object.freeze([]), delivery)
|
|
|
|
|
})
|
|
|
|
|
// Published only after the abort owner and pending done are installed: a
|
|
|
|
|
// dequeue listener that cancels or disposes must find live cancellation
|
|
|
|
|
@@ -457,6 +491,7 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
admitted: UserMessage[] = [],
|
|
|
|
|
inheritedOutboxLength = 0,
|
|
|
|
|
priorFailures: readonly LlmFailure[] = Object.freeze([]),
|
|
|
|
|
promptDelivery?: SteeringDelivery,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
// Both entries hold the invariant: kick() clears the admission slot before
|
|
|
|
|
// awaiting run(), and a retry is entered only after the prior run clears it.
|
|
|
|
|
@@ -464,6 +499,7 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
if (this.abort !== undefined) throw new Error(`agent "${this.id}" is already running`)
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
this.abort = controller
|
|
|
|
|
this.preservePendingAdmissionsOnAbort = false
|
|
|
|
|
this.acceptsNextStep = true
|
|
|
|
|
const signal = controller.signal
|
|
|
|
|
const turn = this.lastTurn + 1
|
|
|
|
|
@@ -487,13 +523,12 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
// Context or steering retained by an earlier rejected admission happened
|
|
|
|
|
// before this prompt and must occupy the same order in durable history.
|
|
|
|
|
this.drainOutbox(turn, inheritedOutboxLength)
|
|
|
|
|
if (promptDelivery !== undefined) this.pendingAdmissions.push(promptDelivery)
|
|
|
|
|
for (const input of admitted) {
|
|
|
|
|
this.session.append('user/message', input, { surfaceOp: 'append' })
|
|
|
|
|
}
|
|
|
|
|
signal.throwIfAborted()
|
|
|
|
|
|
|
|
|
|
this.drainOutbox(turn)
|
|
|
|
|
|
|
|
|
|
steps: while (true) {
|
|
|
|
|
step += 1
|
|
|
|
|
const outcome = await this.step(turn, step, signal)
|
|
|
|
|
@@ -501,17 +536,19 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
case 'completed':
|
|
|
|
|
requestFailureHistory = Object.freeze([])
|
|
|
|
|
if (outcome.maxTokens) reason = { kind: 'max-tokens' }
|
|
|
|
|
// A concluding tool result is terminal: steering already in the
|
|
|
|
|
// log waits for the next turn's request instead of reopening this
|
|
|
|
|
// one, and the agent/turn-stopping drain below is skipped for the same
|
|
|
|
|
// reason.
|
|
|
|
|
if (outcome.concluded) break steps
|
|
|
|
|
// A concluding tool result is terminal: reject steering that did
|
|
|
|
|
// not enter a request, while retaining same-boundary context in
|
|
|
|
|
// durable history before the turn closes.
|
|
|
|
|
if (outcome.concluded) {
|
|
|
|
|
this.discardOutboxSteering()
|
|
|
|
|
this.drainOutbox(turn)
|
|
|
|
|
break steps
|
|
|
|
|
}
|
|
|
|
|
if (outcome.continueTurn || this.outbox.some(item => item.steering)) continue
|
|
|
|
|
break
|
|
|
|
|
case 'request-failed': {
|
|
|
|
|
// step() reports request failures only after step/start commits
|
|
|
|
|
// and before its own step/end, so the step is always open here.
|
|
|
|
|
this.strictSteeringOpen = false
|
|
|
|
|
this.stepOpen = false
|
|
|
|
|
this.session.append('step/end', { turn, step })
|
|
|
|
|
if (!signal.aborted) {
|
|
|
|
|
@@ -542,12 +579,14 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
}
|
|
|
|
|
await this.loopCtx.serial(agentCarrier(this), 'agent/turn-stopping', this, turn, signal)
|
|
|
|
|
signal.throwIfAborted()
|
|
|
|
|
if (!this.drainOutbox(turn)) break
|
|
|
|
|
this.drainOutboxContexts()
|
|
|
|
|
if (!this.outbox.some(item => item.steering)) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (caught: unknown) {
|
|
|
|
|
try {
|
|
|
|
|
if (this.stepOpen) {
|
|
|
|
|
this.strictSteeringOpen = false
|
|
|
|
|
this.stepOpen = false
|
|
|
|
|
this.session.append('step/end', { turn, step })
|
|
|
|
|
}
|
|
|
|
|
@@ -565,7 +604,6 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
// failure paths (step(), the request-failed branch, the catch), so the
|
|
|
|
|
// finally owes only the turn boundary.
|
|
|
|
|
this.acceptsNextStep = false
|
|
|
|
|
this.strictSteeringOpen = false
|
|
|
|
|
try {
|
|
|
|
|
if (this.turnOpen) {
|
|
|
|
|
// Re-entrant turn/end listeners must route new input to a later turn.
|
|
|
|
|
@@ -582,6 +620,10 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
// is still this run's controller here.
|
|
|
|
|
this.abort = undefined
|
|
|
|
|
signal.removeEventListener('abort', cancelRetry)
|
|
|
|
|
const preservePending = signal.aborted && this.preservePendingAdmissionsOnAbort
|
|
|
|
|
this.preservePendingAdmissionsOnAbort = false
|
|
|
|
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- keepInbox cancellation can set this while turn work is awaited.
|
|
|
|
|
if (!preservePending) this.rejectPendingAdmissions()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opened) {
|
|
|
|
|
@@ -620,10 +662,6 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
await this.loopCtx.serial(agentCarrier(this), 'agent/step', this, turn, step, signal)
|
|
|
|
|
signal.throwIfAborted()
|
|
|
|
|
|
|
|
|
|
// Take the outbox whole — same-boundary steering and context leave in
|
|
|
|
|
// this request together.
|
|
|
|
|
this.drainOutbox(turn)
|
|
|
|
|
|
|
|
|
|
// Assemble request-owned prompt inputs fresh each step. Dynamic context is
|
|
|
|
|
// committed at the tail before deriving history once, preserving the stable
|
|
|
|
|
// system/history cache prefix while keeping every model-visible byte logged.
|
|
|
|
|
@@ -632,13 +670,18 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
const system = renderPrompt(assembly)
|
|
|
|
|
materializeRuntimeContext(session, renderContextSnapshot(assembly))
|
|
|
|
|
|
|
|
|
|
// Commit the exact pending batch only after every asynchronous
|
|
|
|
|
// pre-request contribution succeeded. Input accepted after this splice
|
|
|
|
|
// remains pending for a later request.
|
|
|
|
|
this.drainOutbox(turn)
|
|
|
|
|
|
|
|
|
|
// Snapshot the exact log prefix: the reconstruction boundary. Appends
|
|
|
|
|
// after this synchronous snapshot join the next request.
|
|
|
|
|
const boundaryMessages = session.deriveMessages()
|
|
|
|
|
|
|
|
|
|
session.append('step/start', { turn, step })
|
|
|
|
|
this.stepOpen = true
|
|
|
|
|
this.strictSteeringOpen = true
|
|
|
|
|
this.admitPendingAdmissions(turn, step)
|
|
|
|
|
signal.throwIfAborted()
|
|
|
|
|
|
|
|
|
|
const { request, preparedCall } = await this.buildRequest(
|
|
|
|
|
@@ -705,15 +748,14 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tool results stay adjacent to their calls; input accepted during the
|
|
|
|
|
// request enters the log only after the complete result batch.
|
|
|
|
|
this.strictSteeringOpen = false
|
|
|
|
|
const steered = this.drainOutbox(turn)
|
|
|
|
|
// Ordinary context keeps the base loop's result-adjacent commit point.
|
|
|
|
|
// Steering remains provisional until the next request snapshot admits it.
|
|
|
|
|
this.drainOutboxContexts()
|
|
|
|
|
session.append('step/end', { turn, step })
|
|
|
|
|
this.stepOpen = false
|
|
|
|
|
return {
|
|
|
|
|
kind: 'completed',
|
|
|
|
|
continueTurn: (toolCalls.length > 0 && !concluded) || steered,
|
|
|
|
|
continueTurn: (toolCalls.length > 0 && !concluded) || this.outbox.some(item => item.steering),
|
|
|
|
|
concluded,
|
|
|
|
|
maxTokens: finish.kind === 'max-tokens',
|
|
|
|
|
}
|
|
|
|
|
@@ -818,25 +860,83 @@ export class ReactLoopAgent implements Agent {
|
|
|
|
|
return { request, ...preparedCall === undefined ? {} : { preparedCall } }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Commit the outbox and report whether it contained steering. */
|
|
|
|
|
private drainOutbox(turn: number, limit = this.outbox.length): boolean {
|
|
|
|
|
let steered = false
|
|
|
|
|
for (const item of this.outbox.splice(0, limit)) {
|
|
|
|
|
if (item.steering) {
|
|
|
|
|
steered = true
|
|
|
|
|
/* 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 },
|
|
|
|
|
{ surfaceOp: 'append' },
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
this.session.append('user/message', item.message, { surfaceOp: 'append' })
|
|
|
|
|
/** Commit one stable outbox prefix and retain tracked delivery until snapshot admission. */
|
|
|
|
|
private drainOutbox(turn: number, limit = this.outbox.length): void {
|
|
|
|
|
const batch = this.outbox.splice(0, limit)
|
|
|
|
|
for (let index = 0; index < batch.length; index += 1) {
|
|
|
|
|
const item = batch[index]
|
|
|
|
|
/* v8 ignore next -- the index walks the exact array length. */
|
|
|
|
|
if (item === undefined) throw new Error(`agent "${this.id}" outbox item disappeared during drain`)
|
|
|
|
|
try {
|
|
|
|
|
if (item.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 },
|
|
|
|
|
{ surfaceOp: 'append' },
|
|
|
|
|
)
|
|
|
|
|
if (item.delivery !== undefined) this.pendingAdmissions.push(item.delivery)
|
|
|
|
|
} else {
|
|
|
|
|
this.session.append('user/message', item.message, { surfaceOp: 'append' })
|
|
|
|
|
}
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
item.delivery?.settle({ status: 'rejected' })
|
|
|
|
|
this.outbox.unshift(...batch.slice(item.steering ? index + 1 : index))
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return steered
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Commit ordinary context while retaining provisional steering in order. */
|
|
|
|
|
private drainOutboxContexts(): void {
|
|
|
|
|
const pending = this.outbox
|
|
|
|
|
this.outbox = []
|
|
|
|
|
for (let index = 0; index < pending.length; index += 1) {
|
|
|
|
|
const item = pending[index]
|
|
|
|
|
/* v8 ignore next -- the index walks the exact array length. */
|
|
|
|
|
if (item === undefined) throw new Error(`agent "${this.id}" outbox item disappeared during context drain`)
|
|
|
|
|
if (item.steering) {
|
|
|
|
|
this.outbox.push(item)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
this.session.append('user/message', item.message, { surfaceOp: 'append' })
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
this.outbox.push(...pending.slice(index))
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Settle every committed steering item captured by this immutable request. */
|
|
|
|
|
private admitPendingAdmissions(turn: number, step: number): void {
|
|
|
|
|
const outcome: SteeringOutcome = { status: 'admitted', turn, step }
|
|
|
|
|
for (const delivery of this.pendingAdmissions.splice(0)) delivery.settle(outcome)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Reject committed steering that left the inbox without reaching a request. */
|
|
|
|
|
private rejectPendingAdmissions(): void {
|
|
|
|
|
for (const delivery of this.pendingAdmissions.splice(0)) delivery.settle({ status: 'rejected' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Discard uncommitted steering while retaining same-boundary injected context. */
|
|
|
|
|
private discardOutboxSteering(): void {
|
|
|
|
|
const contexts: typeof this.outbox = []
|
|
|
|
|
const discarded: InboxItem[] = []
|
|
|
|
|
for (const item of this.outbox) {
|
|
|
|
|
if (!item.steering) {
|
|
|
|
|
contexts.push(item)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
item.delivery?.settle({ status: 'rejected' })
|
|
|
|
|
/* 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`)
|
|
|
|
|
discarded.push(item.item)
|
|
|
|
|
}
|
|
|
|
|
this.outbox = contexts
|
|
|
|
|
if (discarded.length > 0) emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', discarded)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|