fix: reconcile agent loop merge integration
This commit is contained in:
@@ -27,7 +27,7 @@ import type {
|
||||
import {
|
||||
BlockAssembler, LlmError, assertNever, deepFreeze, errorChain, isHarnessError, llmFailureOf, markAgentLoopRequest,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, LlmCallConfig, LlmFailure, Message } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, LlmCallConfig, LlmFailure, Message, PreparedLlmCall } from '@deepseek-ai/dsh-llm'
|
||||
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionId, TurnEndReason, TurnTrigger, UserMessageData } from '@deepseek-ai/dsh-session'
|
||||
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
@@ -68,6 +68,8 @@ export class ReactLoopAgent implements Agent {
|
||||
/** Whether the session log is owed a matching turn end event. */
|
||||
private turnOpen = false
|
||||
private stepOpen = false
|
||||
/** Whether this loop instance has appended its initial/resume request anchor. */
|
||||
private requestHeaderLogged = false
|
||||
|
||||
constructor(
|
||||
private loopCtx: Context,
|
||||
@@ -445,11 +447,13 @@ export class ReactLoopAgent implements Agent {
|
||||
this.stepOpen = true
|
||||
signal.throwIfAborted()
|
||||
|
||||
const request = await this.buildRequest(turn, step, assembly.tools, system, boundaryMessages, signal)
|
||||
const { request, preparedCall } = await this.buildRequest(
|
||||
turn, step, assembly.tools, system, boundaryMessages, signal,
|
||||
)
|
||||
|
||||
const assembler = new BlockAssembler()
|
||||
const chunkSeqs: number[] = []
|
||||
const stream = this.loopCtx.llm.stream(request)
|
||||
const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request)
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
signal.throwIfAborted()
|
||||
@@ -518,9 +522,8 @@ export class ReactLoopAgent implements Agent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose one frozen request: the `agent/request` config waterfall, the
|
||||
* canonical logged header, then the header plus the boundary snapshot,
|
||||
* byte-for-byte.
|
||||
* Compose one frozen request and bind it to the adapter registration that
|
||||
* resolved its exact-model defaults.
|
||||
*/
|
||||
private async buildRequest(
|
||||
turn: number,
|
||||
@@ -529,40 +532,69 @@ export class ReactLoopAgent implements Agent {
|
||||
system: string,
|
||||
boundaryMessages: Message[],
|
||||
signal: AbortSignal,
|
||||
): Promise<GenerateOptions> {
|
||||
): Promise<{ request: GenerateOptions; preparedCall?: PreparedLlmCall }> {
|
||||
const { session } = this
|
||||
|
||||
// Seed from the logged header when the log has one (the log is the
|
||||
// truth, across resumes too), else from agent options; freeze so
|
||||
// listeners must return a replacement.
|
||||
const loggedConfig = session.requestHeader()?.config
|
||||
const initialProvider = this.options.provider ?? ''
|
||||
const initialModel = this.options.model ?? ''
|
||||
const initialConfig: LlmCallConfig = {
|
||||
provider: initialProvider,
|
||||
model: initialModel,
|
||||
...loggedConfig?.provider === initialProvider
|
||||
&& loggedConfig.model === initialModel
|
||||
&& loggedConfig.reasoningEffort !== undefined
|
||||
? { reasoningEffort: loggedConfig.reasoningEffort }
|
||||
: {},
|
||||
}
|
||||
// A loop instance starts from its declared route, restoring only an opaque
|
||||
// effort owned by that exact model. Later steps fold the config it logged.
|
||||
const seedConfig: LlmCallConfig = deepFreeze(structuredClone(
|
||||
session.requestHeader()?.config
|
||||
?? { provider: this.options.provider ?? '', model: this.options.model ?? '' }))
|
||||
const config = await this.loopCtx.waterfall(
|
||||
this.requestHeaderLogged
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a logged instance anchor guarantees the fold
|
||||
? session.requestHeader()!.config
|
||||
: initialConfig,
|
||||
))
|
||||
const proposedConfig = await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request', this, turn, step, signal,
|
||||
() => Promise.resolve(seedConfig),
|
||||
)
|
||||
signal.throwIfAborted()
|
||||
if (!config.provider || !config.model) {
|
||||
if (!proposedConfig.provider || !proposedConfig.model) {
|
||||
throw new Error(`agent "${this.id}" has no provider/model: set AgentOptions.provider and AgentOptions.model or supply both via the agent/request waterfall`)
|
||||
}
|
||||
let config: LlmCallConfig
|
||||
let preparedCall: PreparedLlmCall | undefined
|
||||
try {
|
||||
preparedCall = await this.loopCtx.llm.prepareCall(proposedConfig, signal)
|
||||
config = preparedCall.config
|
||||
} catch (error: unknown) {
|
||||
// A llm/stream listener may own and short-circuit a route with no
|
||||
// adapter. Terminal dispatch still raises NO_ADAPTER when none does.
|
||||
if (!(error instanceof LlmError) || error.code !== 'NO_ADAPTER') throw error
|
||||
config = proposedConfig
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
|
||||
const header = canonicalHeader({
|
||||
config,
|
||||
...system ? { system } : {},
|
||||
...tools.length > 0 ? { tools } : {},
|
||||
})
|
||||
// Log the header the request will use only when it differs
|
||||
// from the folded baseline — reconstruction folds the log, so an
|
||||
// unchanged header needs no new snapshot.
|
||||
const baseline = session.requestHeader()
|
||||
if (baseline === undefined || !headerEquals(baseline, header)) {
|
||||
session.append('request/header', { header, reason: baseline === undefined ? 'initial' : 'change' })
|
||||
if (!this.requestHeaderLogged) {
|
||||
session.append('request/header', { header, reason: baseline === undefined ? 'initial' : 'resume' })
|
||||
this.requestHeaderLogged = true
|
||||
} else if (baseline === undefined || !headerEquals(baseline, header)) {
|
||||
session.append('request/header', { header, reason: 'change' })
|
||||
}
|
||||
|
||||
return markAgentLoopRequest(deepFreeze({
|
||||
const request = markAgentLoopRequest(deepFreeze({
|
||||
provider: header.config.provider,
|
||||
model: header.config.model,
|
||||
...header.config.reasoningEffort !== undefined
|
||||
? { reasoningEffort: header.config.reasoningEffort }
|
||||
: {},
|
||||
messages: boundaryMessages,
|
||||
...header.system !== undefined ? { system: header.system } : {},
|
||||
...header.tools !== undefined ? { tools: header.tools } : {},
|
||||
@@ -572,6 +604,7 @@ export class ReactLoopAgent implements Agent {
|
||||
sessionId: session.id,
|
||||
signal,
|
||||
}))
|
||||
return { request, ...preparedCall === undefined ? {} : { preparedCall } }
|
||||
}
|
||||
|
||||
/** Commit the outbox and report whether it contained steering. */
|
||||
|
||||
Reference in New Issue
Block a user