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