can1357/oh-my-pi · error

Soft tool requirement '${softRequiredTool}' was not satisfie

Error message

Soft tool requirement '${softRequiredTool}' was not satisfied after ${MAX_SOFT_TOOL_ESCALATIONS} forced turns; aborting to avoid an unbounded force loop.

What it means

When a session declares a soft-required tool (a tool the model MUST call, e.g. to resolve a pending user action), the loop pairs any non-compliant model turn with skipped tool results and forces the required tool via toolChoice on the next turn. After MAX_SOFT_TOOL_ESCALATIONS (3) forced turns the model still has not complied, so the loop throws to prevent an infinite force loop burning tokens.

Source

Thrown at packages/agent/src/agent-loop.ts:1404

					hasMoreToolCalls = false;
				}

				// A turn is compliant ONLY when it calls the required tool and nothing
				// else — mirroring the forced-tool_choice turn, which can emit only that
				// tool. A required+detour batch is treated as non-compliant so detour
				// tools never run side effects while the requirement is still pending.
				const calledOnlyRequiredTool =
					softRequiredTool !== undefined &&
					toolCalls.length > 0 &&
					toolCalls.every(toolCall => softSatisfies?.(toolCall) ?? toolCall.name === softRequiredTool);
				const softGateActive =
					softRequiredTool !== undefined && !hardToolChoiceBlocks(config.toolChoice, softRequiredTool);
				const softNonCompliant = softGateActive && !calledOnlyRequiredTool;

				const toolResults: ToolResultMessage[] = [];
				if (softNonCompliant && softRequiredTool !== undefined) {
					if (softRequirementState.escalations >= MAX_SOFT_TOOL_ESCALATIONS) {
						throw new Error(
							`Soft tool requirement '${softRequiredTool}' was not satisfied after ${MAX_SOFT_TOOL_ESCALATIONS} forced turns; aborting to avoid an unbounded force loop.`,
						);
					}
					// A soft-required tool is pending but the model called something else
					// (or yielded). Do NOT execute the detour — pair each call with a
					// skipped result and force the required tool next turn. This is the
					// only turn that changes toolChoice; a model that complies with the
					// reminder pays no message-cache invalidation. Re-engage so the loop
					// never yields while the requirement is unmet.
					for (const toolCall of toolCalls) {
						const result = createAbortedToolResult(
							toolCall,
							stream,
							"skipped",
							`Not executed: call the \`${softRequiredTool}\` tool to resolve the pending action before using other tools.`,
						);
						currentContext.messages.push(result);
						newMessages.push(result);

View on GitHub (pinned to 9690622007)

Solutions

  1. Check that the required tool name matches a registered tool exactly and its schema is simple enough for the model to satisfy
  2. Inspect the session transcript: the preceding turns contain 'skipped' tool results explaining what the model must call — fix whatever the model keeps doing instead
  3. Remove or reconsider the soft tool requirement if it is not actually mandatory for the task, or raise MAX_SOFT_TOOL_ESCALATIONS
  4. Use a stronger model that reliably honors forced tool_choice, or switch the requirement to a hard toolChoice so the provider enforces it server-side

Example fix

// before: model keeps calling 'read_file' when 'apply_patch' is soft-required
// -> throws after 3 forced turns
// after: enforce it hard so the provider guarantees the call
// before
const agent = new Agent({ toolChoice: "auto" /* soft requirement forced manually */ });
// after
const agent = new Agent({ toolChoice: { type: "tool", name: "apply_patch" } });
Defensive patterns

Strategy: try-catch

Validate before calling

// Before enabling a soft requirement, confirm the tool exists and is callable
const required = "apply_patch";
const registered = agent.getTools().some(t => t.name === required);
if (!registered) throw new Error(`soft-required tool '${required}' is not registered`);

Type guard

function isSoftToolEscalationError(err: unknown): err is Error {
  return err instanceof Error && err.message.startsWith("Soft tool requirement '") && err.message.includes("forced turns");
}

Try / catch

try {
  await agent.prompt(input);
} catch (err) {
  if (isSoftToolEscalationError(err)) {
    // read transcript 'skipped' tool results to see what the model did instead,
    // then either drop the requirement or escalate to a hard toolChoice
    logger.warn("model never satisfied soft requirement", { tool: err.message });
  } else throw err;
}

Prevention

When it happens

Trigger: A soft tool requirement is active (set via the soft-required-tools/dialect gating mechanism) and for 3 consecutive escalated turns the model either yields text, calls no tools, or calls other tools instead of the required one — each detour is skipped and forced, and on the 4th non-compliance the error is thrown.

Common situations: A weak or quantized model repeatedly ignoring forced tool_choice; the required tool's schema confusing the model so it calls a similar tool instead; a tool name mismatch between the requirement and the registered tool list; hard toolChoice config that blocks the forced tool (softGateActive checks hardToolChoiceBlocks).

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3a29641bedaaac7d. Report an issue: GitHub.