fix(core): continue after malformed tool input (#37701)

This commit is contained in:
Kit Langton 2026-07-18 23:09:08 -04:00 committed by GitHub
commit a288cb5a0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 47 additions and 50 deletions

View file

@ -80,7 +80,6 @@ const layer = Layer.effect(
sessionID: SessionSchema.ID,
promotion: SessionPending.Delivery | undefined,
step: number,
recoverMalformedToolInput: boolean,
recoverOverflow?: typeof compaction.compact,
assistantMessageID?: SessionMessage.ID,
) {
@ -144,6 +143,10 @@ const layer = Layer.effect(
}
}
yield* publish(event)
if (LLMEvent.is.toolInputError(event)) {
if (prepared.resolveToolCall(event.name).type === "settle") needsContinuation = true
return
}
if (event.type !== "tool-call" || event.providerExecuted) return
const tool = prepared.resolveToolCall(event.name)
if (tool.type === "reject") {
@ -340,27 +343,15 @@ const layer = Layer.effect(
)
}
const recoveredMalformedToolInput =
recoverMalformedToolInput &&
publisher.hasMalformedToolInput() &&
stream._tag === "Success" &&
stepSettlement !== undefined &&
!providerFailed &&
!streamInterrupted &&
!userDeclined &&
!toolsInterrupted &&
infraError === undefined
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
if (userDeclined) return yield* Effect.interrupt
if ((toolsInterrupted || infraError !== undefined) && settledFailure)
return yield* Effect.failCause(settledFailure)
if (toolsInterrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
if (stepFailure && !recoveredMalformedToolInput) return yield* new StepFailedError({ error: stepFailure })
if (stepFailure) return yield* new StepFailedError({ error: stepFailure })
return {
_tag: "Completed",
needsContinuation: needsContinuation || recoveredMalformedToolInput,
malformedToolInput: recoveredMalformedToolInput,
needsContinuation,
step: currentStep,
} as const
}),
@ -371,7 +362,6 @@ const layer = Layer.effect(
sessionID: SessionSchema.ID,
promotion: SessionPending.Delivery | undefined,
step: number,
recoverMalformedToolInput: boolean,
) {
// Compaction restarts rebuild the request from compacted history without re-promoting.
// Overflow recovery is one-shot: a post-compaction attempt must not recover another
@ -382,14 +372,7 @@ const layer = Layer.effect(
let assistantMessageID: SessionMessage.ID | undefined
while (true) {
const attempt = yield* Effect.suspend(() =>
attemptStep(
sessionID,
currentPromotion,
currentStep,
recoverMalformedToolInput,
recoverOverflow,
assistantMessageID,
),
attemptStep(sessionID, currentPromotion, currentStep, recoverOverflow, assistantMessageID),
).pipe(
Effect.tapError((error) =>
error instanceof SessionRunnerRetry.RetryableFailure
@ -414,7 +397,6 @@ const layer = Layer.effect(
if (attempt._tag === "Completed")
return {
needsContinuation: attempt.needsContinuation,
malformedToolInput: attempt.malformedToolInput,
step: attempt.step,
}
if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined
@ -474,18 +456,12 @@ const layer = Layer.effect(
while (shouldRun) {
let needsContinuation = true
let step = 1
let canRecoverMalformedToolInput = true
// Repeat steps while continuation is needed. A step needs continuation only
// when it recorded local tool calls whose results the model has not yet seen;
// a provider error suppresses it. Pending steers also continue the loop so
// interjections are answered before the session goes idle.
while (needsContinuation) {
const result = yield* runStep(
input.sessionID,
promotion,
step,
canRecoverMalformedToolInput,
)
const result = yield* runStep(input.sessionID, promotion, step)
// Steer/queue promotion inside runStep has already made the pending input a visible
// user message by this point, so the first-user-message check below is reliable.
if (!titleAttempted.has(input.sessionID)) {
@ -493,7 +469,6 @@ const layer = Layer.effect(
forkTitle(title.generateForFirstPrompt(yield* getSession(input.sessionID)).pipe(Effect.ignore))
}
needsContinuation = result.needsContinuation
if (result.malformedToolInput) canRecoverMalformedToolInput = false
step = result.step + 1
if (needsContinuation) {
yield* runPendingCompaction(input.sessionID)

View file

@ -61,7 +61,6 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
let stepFailed = false
let providerFailed = false
let retryEvidence = false
let malformedToolInput = false
let stepFailure: SessionError.Error | undefined
let stepSettlement:
| {
@ -216,7 +215,6 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
readonly id: string
readonly name: string
readonly raw: string
readonly message: string
}) {
if (!tools.has(event.id)) yield* startToolInput(event)
const tool = tools.get(event.id)
@ -226,7 +224,6 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (toolInput.has(event.id)) yield* endToolInput(event, event.raw)
tool.settled = true
malformedToolInput = true
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
@ -237,7 +234,6 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
},
executed: false,
})
if (stepFailure === undefined) stepFailure = { type: "provider.invalid-output", message: event.message }
})
const flush = Effect.fn("SessionRunner.flush")(function* () {
@ -482,7 +478,6 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
publishStepFailure,
failUnsettledTools,
hasProviderError: () => providerFailed,
hasMalformedToolInput: () => malformedToolInput,
hasRetryEvidence: () => retryEvidence,
stepFailure: () => stepFailure,
stepSettlement: () => stepSettlement,