mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(agent-loop): return request failures from steps
This commit is contained in:
@@ -23,7 +23,7 @@ import type {
|
||||
SendOptions,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import {
|
||||
BlockAssembler, LlmError, deepFreeze, errorChain, isHarnessError, llmFailureOf, markAgentLoopRequest,
|
||||
BlockAssembler, LlmError, assertNever, deepFreeze, errorChain, isHarnessError, llmFailureOf, markAgentLoopRequest,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
ContentBlock, GenerateOptions, LlmCallConfig, LlmFailure, Message,
|
||||
@@ -34,16 +34,10 @@ import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import { executeToolCalls } from './tool-calls.ts'
|
||||
|
||||
|
||||
/** A final-adapter or terminal in-band failure eligible for request recovery. */
|
||||
class ModelRequestFailure extends Error {
|
||||
constructor(
|
||||
readonly requestError: RequestError,
|
||||
readonly failure: LlmFailure,
|
||||
) {
|
||||
super(requestError.message, { cause: requestError })
|
||||
}
|
||||
}
|
||||
/** One completed step or a final-adapter failure eligible for recovery. */
|
||||
type StepOutcome =
|
||||
| { kind: 'completed'; continueTurn: boolean; maxTokens: boolean }
|
||||
| { kind: 'request-failed'; error: RequestError; failure: LlmFailure }
|
||||
|
||||
/**
|
||||
* The concrete {@link Agent}: each `run()` owns one turn and repeats model
|
||||
@@ -245,45 +239,59 @@ export class ReactLoopAgent extends Agent {
|
||||
|
||||
this.drainOutbox(turn)
|
||||
|
||||
while (true) {
|
||||
steps: while (true) {
|
||||
step += 1
|
||||
const { continueTurn, maxTokens } = await this.step(turn, step, signal)
|
||||
if (maxTokens) reason = { kind: 'max-tokens' }
|
||||
if (continueTurn || this.outbox.some(item => 'id' in item)) continue
|
||||
const outcome = await this.step(turn, step, signal)
|
||||
switch (outcome.kind) {
|
||||
case 'completed':
|
||||
if (outcome.maxTokens) reason = { kind: 'max-tokens' }
|
||||
if (outcome.continueTurn || this.outbox.some(item => 'id' in item)) continue
|
||||
break
|
||||
case 'request-failed': {
|
||||
if (this.stepOpen) {
|
||||
this.stepOpen = false
|
||||
this.session.append('step/end', { turn, step })
|
||||
}
|
||||
if (agentInterruptReasonOf(signal) === undefined) {
|
||||
const retryWindow = { requested: false }
|
||||
this.retryWindow = retryWindow
|
||||
let recoveryCompleted = false
|
||||
try {
|
||||
await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request-error', this, turn, step, outcome.error,
|
||||
outcome.failure, signal,
|
||||
() => Promise.resolve(),
|
||||
)
|
||||
recoveryCompleted = true
|
||||
} catch (recoveryError: unknown) {
|
||||
this.loopCtx.logger.warn(
|
||||
`agent "${this.id}": request recovery failed at turn ${turn}, step ${step}: ${errorChain(recoveryError)}`,
|
||||
)
|
||||
} finally {
|
||||
if (this.retryWindow === retryWindow) this.retryWindow = undefined
|
||||
}
|
||||
retry = recoveryCompleted
|
||||
&& agentInterruptReasonOf(signal) === undefined
|
||||
&& retryWindow.requested
|
||||
}
|
||||
const settlement = this.settle(turn, step, outcome.error, signal, outcome.failure)
|
||||
reason = settlement.reason
|
||||
idle = settlement.idle
|
||||
break steps
|
||||
}
|
||||
default:
|
||||
assertNever(outcome)
|
||||
}
|
||||
await this.loopCtx.serial(agentCarrier(this), 'agent/stopping', this, turn, signal)
|
||||
signal.throwIfAborted()
|
||||
if (!this.drainOutbox(turn)) break
|
||||
}
|
||||
} catch (caught: unknown) {
|
||||
const requestFailure = caught instanceof ModelRequestFailure ? caught : undefined
|
||||
const error = requestFailure?.requestError ?? caught
|
||||
if (this.stepOpen) {
|
||||
this.stepOpen = false
|
||||
this.session.append('step/end', { turn, step })
|
||||
}
|
||||
if (requestFailure !== undefined && agentInterruptReasonOf(signal) === undefined) {
|
||||
const retryWindow = { requested: false }
|
||||
this.retryWindow = retryWindow
|
||||
let recoveryCompleted = false
|
||||
try {
|
||||
await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request-error', this, turn, step, requestFailure.requestError,
|
||||
requestFailure.failure, signal,
|
||||
() => Promise.resolve(),
|
||||
)
|
||||
recoveryCompleted = true
|
||||
} catch (recoveryError: unknown) {
|
||||
this.loopCtx.logger.warn(
|
||||
`agent "${this.id}": request recovery failed at turn ${turn}, step ${step}: ${errorChain(recoveryError)}`,
|
||||
)
|
||||
} finally {
|
||||
if (this.retryWindow === retryWindow) this.retryWindow = undefined
|
||||
}
|
||||
retry = recoveryCompleted
|
||||
&& agentInterruptReasonOf(signal) === undefined
|
||||
&& retryWindow.requested
|
||||
}
|
||||
({ reason, idle } = this.settle(turn, step, error, signal, requestFailure?.failure))
|
||||
({ reason, idle } = this.settle(turn, step, caught, signal))
|
||||
} finally {
|
||||
try {
|
||||
if (this.stepOpen) {
|
||||
@@ -321,7 +329,7 @@ export class ReactLoopAgent extends Agent {
|
||||
turn: number,
|
||||
step: number,
|
||||
signal: AbortSignal,
|
||||
): Promise<{ continueTurn: boolean; maxTokens: boolean }> {
|
||||
): Promise<StepOutcome> {
|
||||
const { session } = this
|
||||
|
||||
// The single between-steps seam: listeners inject, steer, or edit the log
|
||||
@@ -361,7 +369,7 @@ export class ReactLoopAgent extends Agent {
|
||||
} catch (error: unknown) {
|
||||
const facts = llmFailureOf(stream, error)
|
||||
if (facts !== undefined && error instanceof Error) {
|
||||
throw new ModelRequestFailure(error, facts)
|
||||
return { kind: 'request-failed', error, failure: facts }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -371,7 +379,7 @@ export class ReactLoopAgent extends Agent {
|
||||
const finish = assembler.finish
|
||||
if (finish.kind === 'error' || finish.kind === 'aborted') {
|
||||
const error = new LlmError(finish.failure.message, finish.failure.code, finish.failure)
|
||||
throw new ModelRequestFailure(error, finish.failure)
|
||||
return { kind: 'request-failed', error, failure: finish.failure }
|
||||
}
|
||||
|
||||
// Truncated (max-tokens) output cannot owe tool calls.
|
||||
@@ -411,6 +419,7 @@ export class ReactLoopAgent extends Agent {
|
||||
session.append('step/end', { turn, step })
|
||||
this.stepOpen = false
|
||||
return {
|
||||
kind: 'completed',
|
||||
continueTurn: (toolCalls.length > 0 && !concluded) || steered,
|
||||
maxTokens: finish.kind === 'max-tokens',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user