Files
whetstone_DSL/tools/mcp/ollama_mcp_taskitem_executor.sh

1273 lines
49 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Bridge: Ollama model <-> Whetstone MCP tools with explicit
# tool_selection -> tool_execution stage logging.
#
# Usage:
# ./tools/mcp/ollama_mcp_taskitem_executor.sh <taskitems_json> [task_index]
#
# Examples:
# ./tools/mcp/ollama_mcp_taskitem_executor.sh logs/taskitem_runs/.../02_generate_taskitems.json
# ./tools/mcp/ollama_mcp_taskitem_executor.sh logs/taskitem_runs/.../02_generate_taskitems.json 0
#
# Environment:
# OLLAMA_MODEL default: qwen2.5-coder:14b
# WSTONE_MCP_BIN default: editor/build-native/whetstone_mcp_stable
# WSTONE_WORKSPACE default: repo root
# WSTONE_LANGUAGE default: cpp
# OLLAMA_MAX_TURNS default: 6
# OLLAMA_CLEAR_CACHE_EACH_TURN default: 1
# OLLAMA_KEEP_ALIVE default: 0 (seconds)
# ADAPTIVE_MEMORY default: 1 (enable auto reset/compaction)
# CONTEXT_MAX_EVENTS default: 8 (keep most recent events)
# RESET_ERROR_STREAK default: 3 (same failure signature)
# RESET_PROMPT_TOKENS default: 3200 (prompt token pressure threshold)
if ! command -v jq >/dev/null 2>&1; then
echo "error: jq is required" >&2
exit 1
fi
if ! command -v ollama >/dev/null 2>&1; then
echo "error: ollama not found in PATH" >&2
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TASKITEMS_FILE="${1:-}"
TASK_INDEX="${2:-}"
if [[ -z "$TASKITEMS_FILE" ]]; then
echo "usage: $0 <taskitems_json> [task_index]" >&2
exit 1
fi
if [[ "$TASKITEMS_FILE" != /* ]]; then
TASKITEMS_FILE="$ROOT_DIR/$TASKITEMS_FILE"
fi
if [[ ! -f "$TASKITEMS_FILE" ]]; then
echo "error: taskitems file not found: $TASKITEMS_FILE" >&2
exit 1
fi
TASK_RUN_DIR="$(cd "$(dirname "$TASKITEMS_FILE")" && pwd)"
INTAKE_FILE="$TASK_RUN_DIR/01_intake.json"
INTAKE_RETRY_FILE="$TASK_RUN_DIR/01b_intake_retry.json"
NORMALIZED_REQUIREMENTS_JSON="[]"
if [[ -f "$INTAKE_RETRY_FILE" ]]; then
NORMALIZED_REQUIREMENTS_JSON="$(jq -c '.normalizedRequirements // []' "$INTAKE_RETRY_FILE" 2>/dev/null || echo '[]')"
elif [[ -f "$INTAKE_FILE" ]]; then
NORMALIZED_REQUIREMENTS_JSON="$(jq -c '.normalizedRequirements // []' "$INTAKE_FILE" 2>/dev/null || echo '[]')"
fi
MODEL="${OLLAMA_MODEL:-qwen2.5-coder:14b}"
BIN="${WSTONE_MCP_BIN:-$ROOT_DIR/editor/build-native/whetstone_mcp_stable}"
WORKSPACE="${WSTONE_WORKSPACE:-$ROOT_DIR}"
LANGUAGE="${WSTONE_LANGUAGE:-cpp}"
MAX_TURNS="${OLLAMA_MAX_TURNS:-6}"
CLEAR_CACHE="${OLLAMA_CLEAR_CACHE_EACH_TURN:-1}"
KEEP_ALIVE="${OLLAMA_KEEP_ALIVE:-0}"
OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434/api/generate}"
OLLAMA_CHAT_URL="${OLLAMA_CHAT_URL:-http://127.0.0.1:11434/api/chat}"
MCP_TIMEOUT_SECONDS="${MCP_TIMEOUT_SECONDS:-25}"
CURL_TIMEOUT_SECONDS="${CURL_TIMEOUT_SECONDS:-90}"
OLLAMA_INTERFACE_MODE="${OLLAMA_INTERFACE_MODE:-json_loop}"
ADAPTIVE_MEMORY="${ADAPTIVE_MEMORY:-1}"
CONTEXT_MAX_EVENTS="${CONTEXT_MAX_EVENTS:-8}"
RESET_ERROR_STREAK="${RESET_ERROR_STREAK:-3}"
RESET_PROMPT_TOKENS="${RESET_PROMPT_TOKENS:-3200}"
CYCLE_FAILURE_STREAK="${CYCLE_FAILURE_STREAK:-3}"
ESCALATE_ON_CYCLE="${ESCALATE_ON_CYCLE:-1}"
SMART_MODEL_TAG="${SMART_MODEL_TAG:-planner-llm}"
MAX_INVALID_ACTION_STREAK="${MAX_INVALID_ACTION_STREAK:-2}"
MAX_SUCCESS_REPEAT_STREAK="${MAX_SUCCESS_REPEAT_STREAK:-2}"
if [[ ! -x "$BIN" ]]; then
echo "error: MCP binary not executable: $BIN" >&2
exit 1
fi
STAMP="$(date +%Y%m%d_%H%M%S)"
OUT_DIR="$ROOT_DIR/logs/taskitem_runs/ollama_mcp_exec_${STAMP}_$$"
mkdir -p "$OUT_DIR"
mcp_roundtrip() {
local req_json="$1"
local init_req responses final
init_req="$(jq -nc \
'{jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2025-11-25",capabilities:{},clientInfo:{name:"ollama-mcp-exec",version:"1.0"}}}')"
responses="$(printf '%s\n%s\n' "$init_req" "$req_json" | timeout "${MCP_TIMEOUT_SECONDS}s" "$BIN" --workspace "$WORKSPACE" --language "$LANGUAGE" 2>/dev/null || true)"
final="$(printf '%s\n' "$responses" | tail -n1)"
printf '%s' "$final"
}
list_tools_json() {
local req
req='{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
mcp_roundtrip "$req"
}
call_tool_json() {
local tool_name="$1"
local args_json="$2"
local req
req="$(jq -nc --arg t "$tool_name" --argjson a "$args_json" \
'{jsonrpc:"2.0",id:2,method:"tools/call",params:{name:$t,arguments:$a}}')"
mcp_roundtrip "$req"
}
has_tool() {
local name="$1"
grep -qx "$name" <<< "$tool_names"
}
safe_call_tool() {
local name="$1"
local args="$2"
if ! has_tool "$name"; then
return 1
fi
call_tool_json "$name" "$args"
return 0
}
normalize_args_for_tool() {
local tool_name="$1"
local args_json="$2"
if [[ "$tool_name" == "whetstone_validate_taskitem" ]]; then
printf '%s' "$args_json" | jq -c '
if (.taskitems | type) == "array" then
.taskitems = (
.taskitems | map(
{
task_id: (.task_id // .taskId // ""),
title: (.title // ""),
prerequisite_ops: (.prerequisite_ops // .prerequisiteOps // []),
reasons: (.reasons // []),
confidence: (.confidence // 0),
dependency_task_ids: (.dependency_task_ids // .dependencyTaskIds // [])
}
)
)
else . end
'
return 0
fi
printf '%s' "$args_json"
}
build_intake_markdown_from_task() {
local task_json="$1"
jq -r --argjson t "$task_json" '
[
"## Goals",
"- " + ($t.title // "Task goal"),
"",
"## Constraints",
(($t.prerequisiteOps // []) | map("- prerequisite: " + .) | join("\n")),
"",
"## Dependencies",
(($t.dependencyTaskIds // []) | if length > 0 then map("- task dependency: " + .) | join("\n") else "- none" end),
"",
"## Acceptance Criteria",
(($t.reasons // []) | if length > 0 then map("- " + .) | join("\n") else "- task criteria pending" end)
] | join("\n")
'
}
preflight_args_for_tool() {
local tool_name="$1"
local args_json="$2"
local task_json="$3"
# Deterministic fixes for common schema misses.
if [[ "$tool_name" == "whetstone_validate_taskitem" ]]; then
printf '%s' "$args_json" | jq -c --argjson t "$task_json" --arg ws "$WORKSPACE" '
.taskitems = (
if (.taskitems | type) == "array" and (.taskitems | length) > 0 then
.taskitems
else
[{
task_id: ($t.taskId // "task-1"),
title: ($t.title // "Task"),
prerequisite_ops: ($t.prerequisiteOps // []),
reasons: ($t.reasons // []),
confidence: ($t.confidence // 0),
dependency_task_ids: ($t.dependencyTaskIds // [])
}]
end
)
| .workspace = (.workspace // $ws)
'
return 0
fi
if [[ "$tool_name" == "whetstone_architect_intake" ]]; then
local md
md="$(printf '%s' "$args_json" | jq -r '.markdown // ""')"
if ! grep -Eq '^## (Goals|Constraints|Dependencies|Acceptance Criteria)$' <<< "$md"; then
md="$(build_intake_markdown_from_task "$task_json")"
printf '%s' "$args_json" | jq -c --arg md "$md" '.markdown = $md'
return 0
fi
fi
if [[ "$tool_name" == "whetstone_queue_ready" ]]; then
printf '%s' "$args_json" | jq -c --argjson t "$task_json" --argjson nr "$NORMALIZED_REQUIREMENTS_JSON" '
.tasks = (
if (.tasks | type) == "array" and (.tasks | length) > 0 and (.tasks[0] | type) == "object" then
.tasks
else
[{
taskId: ($t.taskId // "task-1"),
title: ($t.title // "Task"),
queueReady: ($t.queueReady // true),
milestoneId: ($t.milestoneId // "milestone-1"),
escalate: ($t.escalate // false)
}]
end
)
| .normalizedRequirements = (
if (.normalizedRequirements | type) == "array" and (.normalizedRequirements | length) > 0 then
.normalizedRequirements
else
$nr
end
)
'
return 0
fi
printf '%s' "$args_json"
}
extract_tool_error() {
local tool_text="$1"
printf '%s' "$tool_text" | jq -r '
(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $j
| if ($j.success == false and ($j.error|type=="string")) then $j.error else empty end
'
}
extract_tool_issue() {
local tool_text="$1"
printf '%s' "$tool_text" | jq -r '
(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $j
| if ($j.success == false and ($j.error|type=="string")) then
$j.error
elif ($j.error|type=="string") then
$j.error
elif (($j.blockers|type=="array") and (($j.blockers|length) > 0)) then
("blockers:" + ($j.blockers|join(",")))
else
empty
end
'
}
compact_context_file() {
local ctx_file="$1"
local max_events="$2"
if [[ ! -f "$ctx_file" ]]; then
return 0
fi
local tmp
tmp="$(mktemp)"
tail -n "$max_events" "$ctx_file" > "$tmp" || true
mv "$tmp" "$ctx_file"
}
extract_first_json() {
python3 -c '
import json, re, sys
s = sys.stdin.read().strip()
s = re.sub(r"^```(?:json)?\s*|\s*```$", "", s, flags=re.DOTALL).strip()
if not s:
print("{}")
raise SystemExit(0)
try:
obj = json.loads(s)
print(json.dumps(obj))
raise SystemExit(0)
except Exception:
pass
start = s.find("{")
if start < 0:
print("{}")
raise SystemExit(0)
depth = 0
for i, ch in enumerate(s[start:], start=start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
cand = s[start:i+1]
try:
obj = json.loads(cand)
print(json.dumps(obj))
raise SystemExit(0)
except Exception:
break
print("{}")
'
}
parse_tool_args_json() {
local raw="$1"
printf '%s' "$raw" | jq -c '
if type == "string" then
(fromjson? // {})
elif type == "object" then
.
else
{}
end
'
}
json_or_default() {
local raw="$1"
local def="$2"
local out
out="$(printf '%s' "$raw" | jq -c '.' 2>/dev/null || true)"
if [[ -z "$out" ]]; then
printf '%s' "$def"
else
printf '%s' "$out"
fi
}
tool_list_resp="$(list_tools_json)"
printf '%s\n' "$tool_list_resp" > "$OUT_DIR/00_tools_list_raw.json"
tool_names="$(printf '%s' "$tool_list_resp" | jq -r '.result.tools[]?.name' | sort)"
tool_names_json="$(printf '%s\n' "$tool_names" | jq -R -s -c 'split("\n") | map(select(length>0))')"
printf '%s\n' "$tool_names" > "$OUT_DIR/00_tools_list.txt"
tool_schema_summary="$(printf '%s' "$tool_list_resp" | jq -r '
.result.tools[]
| {
name,
required: (.inputSchema.required // []),
properties: ((.inputSchema.properties // {}) | keys)
}
| "- \(.name): required=\(.required|join(",")); properties=\(.properties|join(","))"
')"
printf '%s\n' "$tool_schema_summary" > "$OUT_DIR/00_tools_schema_summary.txt"
ollama_tools_json="$(printf '%s' "$tool_list_resp" | jq -c '
[
.result.tools[]?
| {
type: "function",
function: {
name: .name,
description: (.description // ("MCP tool " + .name)),
parameters: (.inputSchema // {type:"object",properties:{},required:[]})
}
}
]
')"
printf '%s\n' "$ollama_tools_json" > "$OUT_DIR/00_ollama_tools.json"
RUN_SESSION_ID="ollama-mcp-${STAMP}"
if [[ -z "$tool_names" ]]; then
echo "error: no MCP tools discovered from $BIN" >&2
exit 2
fi
task_query='.tasks'
if [[ -n "$TASK_INDEX" ]]; then
if ! [[ "$TASK_INDEX" =~ ^[0-9]+$ ]]; then
echo "error: task_index must be an integer" >&2
exit 1
fi
task_query=".tasks[$TASK_INDEX]"
fi
task_payload="$(jq -c "$task_query" "$TASKITEMS_FILE")"
if [[ -z "$task_payload" || "$task_payload" == "null" ]]; then
echo "error: selected task payload not found in $TASKITEMS_FILE" >&2
exit 3
fi
transcript="$OUT_DIR/01_transcript.log"
printf 'MODEL=%s\nTASKITEM_FILE=%s\nTASK_INDEX=%s\n\n' "$MODEL" "$TASKITEMS_FILE" "${TASK_INDEX:-all}" > "$transcript"
run_one_task() {
local task_json="$1"
local idx="$2"
local ctx_file="$OUT_DIR/task_${idx}_context.jsonl"
local chat_messages_file="$OUT_DIR/task_${idx}_chat_messages.json"
local validate_hint
local made_tool_call=0
local mode="sequential"
local reset_reason=""
local force_reset=0
local last_issue_sig=""
local issue_streak=0
local last_cycle_signature=""
local cycle_streak=0
local cycle_escalated=0
local invalid_action_streak=0
local last_success_tool_sig=""
local success_tool_repeat_streak=0
: > "$ctx_file"
printf '[]\n' > "$chat_messages_file"
echo "=== Task $idx ==="
echo "$task_json" | jq . > "$OUT_DIR/task_${idx}_input.json"
validate_hint="$(jq -nc --argjson t "$task_json" --arg ws "$WORKSPACE" '
{
taskitems: [
{
task_id: ($t.taskId // "task-1"),
title: ($t.title // "Task"),
prerequisite_ops: ($t.prerequisiteOps // []),
reasons: ($t.reasons // []),
confidence: ($t.confidence // 0),
dependency_task_ids: ($t.dependencyTaskIds // [])
}
],
workspace: $ws
}')"
local prereq_hint
prereq_hint="$(jq -n -r --argjson t "$task_json" '
($t.prerequisiteOps // []) as $p
| if ($p | index("validate-intake")) then
"Route: validate-intake => whetstone_validate_taskitem first."
else
"Route: choose tools based on required operations."
end
')"
if [[ "$OLLAMA_INTERFACE_MODE" == "native_tools" ]]; then
jq -nc \
--arg task "$task_json" \
--arg hint "$prereq_hint" \
'[
{
role: "system",
content: (
"You are a small execution model in a tool loop. " +
"Use tool calls when appropriate. " +
"Do not repeat the same successful tool call with identical arguments."
)
},
{
role: "user",
content: (
"TASKITEM:\n" + $task + "\n\n" +
"Route hint: " + $hint + "\n" +
"If blocked, respond with a compact JSON final summary."
)
}
]' > "$chat_messages_file"
fi
# Start a debug/session recording channel if available.
if has_tool "whetstone_start_recording"; then
local rec_args rec_resp
rec_args="$(jq -nc --arg sid "${RUN_SESSION_ID}-task-${idx}" --arg td "$(jq -r '.title // "task"' <<< "$task_json")" \
'{session_id:$sid, task_description:$td}')"
rec_resp="$(safe_call_tool "whetstone_start_recording" "$rec_args" || true)"
{
echo "--- task=$idx bootstrap ---"
echo "debug_probe: whetstone_start_recording $rec_args"
echo "debug_probe_raw:"
printf '%s\n' "$rec_resp"
} >> "$transcript"
fi
emit_failure_debug_probes() {
local reason="$1"
local detail="$2"
local r
local inference_goal
{
echo "--- task=$idx failure_probe ---"
echo "failure_reason: $reason"
echo "failure_detail: $detail"
} >> "$transcript"
if has_tool "whetstone_get_workflow_state"; then
r="$(safe_call_tool "whetstone_get_workflow_state" '{}' || true)"
{
echo "debug_probe: whetstone_get_workflow_state {}"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
if has_tool "whetstone_get_blockers"; then
r="$(safe_call_tool "whetstone_get_blockers" '{}' || true)"
{
echo "debug_probe: whetstone_get_blockers {}"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
if has_tool "whetstone_get_recent_events"; then
r="$(safe_call_tool "whetstone_get_recent_events" '{"count":20}' || true)"
{
echo "debug_probe: whetstone_get_recent_events {\"count\":20}"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
# If explicit debug tools exist in future builds, call them best-effort.
if has_tool "whetstone_record_debug_trace"; then
r="$(safe_call_tool "whetstone_record_debug_trace" "$(jq -nc --arg sid "${RUN_SESSION_ID}-task-${idx}" --arg rr "$reason" --arg dd "$detail" '{session_id:$sid,reason:$rr,detail:$dd}')" || true)"
{
echo "debug_probe: whetstone_record_debug_trace"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
if has_tool "whetstone_classify_debug_stop_reason"; then
r="$(safe_call_tool "whetstone_classify_debug_stop_reason" "$(jq -nc --arg rr "$reason" --arg dd "$detail" '{reason:$rr,detail:$dd}')" || true)"
{
echo "debug_probe: whetstone_classify_debug_stop_reason"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
if has_tool "whetstone_get_recovery_advice"; then
r="$(safe_call_tool "whetstone_get_recovery_advice" "$(jq -nc --arg rr "$reason" --arg dd "$detail" '{reason:$rr,detail:$dd}')" || true)"
{
echo "debug_probe: whetstone_get_recovery_advice"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
# Optional: generate an inference job so failures feed orchestration backlog.
if has_tool "whetstone_generate_inference_job"; then
inference_goal="Bridge failure probe: ${reason} (${detail})"
r="$(safe_call_tool "whetstone_generate_inference_job" "$(jq -nc --arg g "$inference_goal" --arg f1 "$TASKITEMS_FILE" --arg f2 "$transcript" '{goal:$g,entropy_score:70,bounty:"low",files:[$f1,$f2]}')" || true)"
{
echo "debug_probe: whetstone_generate_inference_job"
echo "debug_probe_raw:"
printf '%s\n' "$r"
} >> "$transcript"
fi
}
emit_cycle_event() {
local turn="$1"
local signature="$2"
local stage="$3"
local tool="$4"
local detail="$5"
jq -nc \
--arg stage "failure_cycle" \
--argjson turn "$turn" \
--arg signature "$signature" \
--arg cycle_stage "$stage" \
--arg tool "$tool" \
--arg detail "$detail" \
--argjson streak "$cycle_streak" \
'{
stage:$stage,
turn:$turn,
label:"bad",
error_type:"failure_cycle",
cycle_signature:$signature,
cycle_stage:$cycle_stage,
cycle_tool:(if $tool=="" then null else $tool end),
cycle_streak:$streak,
detail:$detail
}' >> "$ctx_file"
}
update_cycle_signature() {
local turn="$1"
local signature="$2"
local stage="$3"
local tool="$4"
local detail="$5"
if [[ -z "$signature" ]]; then
last_cycle_signature=""
cycle_streak=0
return 0
fi
if [[ "$signature" == "$last_cycle_signature" ]]; then
cycle_streak=$((cycle_streak + 1))
else
last_cycle_signature="$signature"
cycle_streak=1
fi
if (( cycle_streak >= CYCLE_FAILURE_STREAK )); then
emit_cycle_event "$turn" "$signature" "$stage" "$tool" "$detail"
emit_failure_debug_probes "failure_cycle_detected" "$signature"
if [[ "$ESCALATE_ON_CYCLE" == "1" ]]; then
cycle_escalated=1
fi
fi
}
write_failure_cycle_summary() {
local summary_file="$OUT_DIR/task_${idx}_failure_cycles.json"
jq -s '
{
schema_version: 1,
total_events: length,
by_stage: (
map(select(.stage?=="failure_cycle")) | group_by(.cycle_stage // "unknown")
| map({stage:(.[0].cycle_stage // "unknown"), count:length})
),
by_signature: (
map(select(.stage?=="failure_cycle")) | group_by(.cycle_signature // "unknown")
| map({signature:(.[0].cycle_signature // "unknown"), count:length})
),
by_tool: (
map(select(.stage?=="tool_execution" and .label?=="bad" and (.tool|type)=="string"))
| group_by(.tool) | map({tool:.[0].tool, count:length})
)
}' "$ctx_file" > "$summary_file"
}
local turn
for turn in $(seq 1 "$MAX_TURNS"); do
local context_blob selection_prompt raw model_json action tool args tool_resp tool_text tool_err
local tool_issue
local selection_reason selection_confidence selection_fallbacks selection_intent selection_phase selection_label execution_result
local gen_json gen_resp prompt_tokens completion_tokens total_tokens
if [[ "$ADAPTIVE_MEMORY" == "1" && "$force_reset" -eq 1 ]]; then
: > "$ctx_file"
jq -nc --arg reason "$reset_reason" --arg t "$turn" \
'{meta:"context_reset",turn:($t|tonumber),reason:$reason}' >> "$ctx_file"
{
echo "--- task=$idx turn=$turn ---"
echo "memory_mode: reset"
echo "memory_reset_reason: $reset_reason"
} >> "$transcript"
mode="reset"
force_reset=0
else
mode="sequential"
fi
compact_context_file "$ctx_file" "$CONTEXT_MAX_EVENTS"
context_blob="$(cat "$ctx_file" 2>/dev/null || true)"
if [[ "$OLLAMA_INTERFACE_MODE" == "native_tools" ]]; then
local chat_req chat_resp assistant_content assistant_msg tool_calls_count tc_idx
local tool_call_entry tool_call_tool tool_call_args_raw tool_call_args
chat_req="$(jq -nc \
--arg model "$MODEL" \
--arg keep_alive "$KEEP_ALIVE" \
--argjson messages "$(cat "$chat_messages_file")" \
--argjson tools "$ollama_tools_json" \
'{
model: $model,
messages: $messages,
tools: $tools,
stream: false,
keep_alive: $keep_alive,
options: {
temperature: 0.1,
top_p: 0.9,
num_predict: 1024
}
}'
)"
chat_resp="$(curl -s --max-time "$CURL_TIMEOUT_SECONDS" -X POST "$OLLAMA_CHAT_URL" -H "Content-Type: application/json" -d "$chat_req" 2>/dev/null || true)"
prompt_tokens="$(printf '%s' "$chat_resp" | jq -r '.prompt_eval_count // 0')"
completion_tokens="$(printf '%s' "$chat_resp" | jq -r '.eval_count // 0')"
total_tokens=$((prompt_tokens + completion_tokens))
if [[ "$CLEAR_CACHE" == "1" ]]; then
ollama stop "$MODEL" >/dev/null 2>&1 || true
fi
assistant_content="$(printf '%s' "$chat_resp" | jq -r '.message.content // ""' 2>/dev/null || echo "")"
assistant_msg="$(printf '%s' "$chat_resp" | jq -c '.message // {}' 2>/dev/null || echo '{}')"
tool_calls_count="$(printf '%s' "$chat_resp" | jq -r '.message.tool_calls | length // 0' 2>/dev/null || echo 0)"
{
echo "--- task=$idx turn=$turn ---"
echo "native_chat_response:"
printf '%s\n' "$chat_resp"
echo "token_usage: prompt=${prompt_tokens} completion=${completion_tokens} total=${total_tokens} stage=native_tools"
echo "memory_mode: $mode"
} >> "$transcript"
local safe_messages_json safe_assistant_msg_json
safe_messages_json="$(json_or_default "$(cat "$chat_messages_file" 2>/dev/null)" "[]")"
safe_assistant_msg_json="$(json_or_default "$assistant_msg" "{}")"
jq -nc --argjson m "$safe_messages_json" --argjson a "$safe_assistant_msg_json" '$m + [$a]' > "$chat_messages_file"
if (( tool_calls_count > 0 )); then
for tc_idx in $(seq 0 $((tool_calls_count - 1))); do
tool_call_entry="$(printf '%s' "$chat_resp" | jq -c ".message.tool_calls[$tc_idx].function // {}")"
tool_call_tool="$(printf '%s' "$tool_call_entry" | jq -r '.name // ""')"
tool_call_args_raw="$(printf '%s' "$tool_call_entry" | jq -c '.arguments // {}')"
tool_call_args="$(parse_tool_args_json "$tool_call_args_raw" 2>/dev/null || echo '{}')"
selection_intent="native_tool_call"
selection_phase="execution"
selection_reason="native_tool_call"
selection_confidence="1.0"
selection_fallbacks="[]"
selection_label="good"
tool_call_args="$(normalize_args_for_tool "$tool_call_tool" "$tool_call_args")"
tool_call_args="$(preflight_args_for_tool "$tool_call_tool" "$tool_call_args" "$task_json")"
if ! grep -qx "$tool_call_tool" <<< "$tool_names"; then
selection_label="bad"
jq -nc \
--arg stage "tool_selection" \
--argjson turn "$turn" \
--arg intent "$selection_intent" \
--arg phase "$selection_phase" \
--argjson candidates "$tool_names_json" \
--arg chosen_tool "$tool_call_tool" \
--argjson args "$tool_call_args" \
--arg reason "$selection_reason" \
--argjson confidence "$selection_confidence" \
--argjson fallbacks "$selection_fallbacks" \
--arg label "$selection_label" \
--arg error_type "unknown_tool" \
'{stage:$stage,turn:$turn,intent:$intent,task_phase:$phase,candidate_tools:$candidates,chosen_tool:$chosen_tool,args:$args,reason:$reason,confidence:$confidence,fallback_tools:$fallbacks,label:$label,error_type:$error_type}' >> "$ctx_file"
emit_failure_debug_probes "unknown_tool" "$tool_call_tool"
continue
fi
jq -nc \
--arg stage "tool_selection" \
--argjson turn "$turn" \
--arg intent "$selection_intent" \
--arg phase "$selection_phase" \
--argjson candidates "$tool_names_json" \
--arg chosen_tool "$tool_call_tool" \
--argjson args "$tool_call_args" \
--arg reason "$selection_reason" \
--argjson confidence "$selection_confidence" \
--argjson fallbacks "$selection_fallbacks" \
--arg label "$selection_label" \
'{stage:$stage,turn:$turn,intent:$intent,task_phase:$phase,candidate_tools:$candidates,chosen_tool:$chosen_tool,args:$args,reason:$reason,confidence:$confidence,fallback_tools:$fallbacks,label:$label}' >> "$ctx_file"
tool_resp="$(call_tool_json "$tool_call_tool" "$tool_call_args")"
tool_text="$(printf '%s' "$tool_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
tool_err="$(extract_tool_error "$tool_text")"
tool_issue="$(extract_tool_issue "$tool_text")"
made_tool_call=1
execution_result="$(printf '%s' "$tool_text" | jq -r '
(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $r
| if ($r.success == true) then "success"
elif ($r.success == false) then "error"
elif (($r.blockers|type=="array") and (($r.blockers|length)>0)) then "blocked"
else "unknown" end
')"
jq -nc \
--arg stage "tool_execution" \
--argjson turn "$turn" \
--arg tool "$tool_call_tool" \
--argjson args "$tool_call_args" \
--arg text "$tool_text" \
--arg result "$execution_result" \
--arg label "$( [[ -n "$tool_err" ]] && echo "bad" || echo "good" )" \
--arg error_type "${tool_err:-}" \
'{stage:$stage,turn:$turn,tool:$tool,args:$args,result:$result,label:$label,error_type:(if ($error_type|length)>0 then $error_type else null end),result_preview:($text|tostring|.[0:2000])}' >> "$ctx_file"
safe_messages_json="$(json_or_default "$(cat "$chat_messages_file" 2>/dev/null)" "[]")"
jq -nc --argjson m "$safe_messages_json" --arg t "$tool_call_tool" --arg c "$tool_text" \
'$m + [{role:"tool", tool_name:$t, content:$c}]' > "$chat_messages_file"
done
continue
fi
if [[ -n "$assistant_content" ]]; then
model_json="$(printf '%s' "$assistant_content" | extract_first_json)"
action="$(printf '%s' "$model_json" | jq -r '.action // ""')"
# Fallback: some models emit tool call JSON in content instead of tool_calls array.
local fallback_tool fallback_args
fallback_tool="$(printf '%s' "$model_json" | jq -r '.name // ""')"
fallback_args="$(printf '%s' "$model_json" | jq -c '.arguments // {}')"
if [[ -n "$fallback_tool" ]]; then
fallback_args="$(parse_tool_args_json "$fallback_args" 2>/dev/null || echo '{}')"
fallback_args="$(normalize_args_for_tool "$fallback_tool" "$fallback_args")"
fallback_args="$(preflight_args_for_tool "$fallback_tool" "$fallback_args" "$task_json")"
if grep -qx "$fallback_tool" <<< "$tool_names"; then
jq -nc \
--arg stage "tool_selection" \
--argjson turn "$turn" \
--arg intent "native_content_tool_call" \
--arg phase "execution" \
--argjson candidates "$tool_names_json" \
--arg chosen_tool "$fallback_tool" \
--argjson args "$fallback_args" \
--arg reason "content_tool_call_fallback" \
--argjson confidence "1.0" \
--argjson fallbacks "[]" \
--arg label "good" \
'{stage:$stage,turn:$turn,intent:$intent,task_phase:$phase,candidate_tools:$candidates,chosen_tool:$chosen_tool,args:$args,reason:$reason,confidence:$confidence,fallback_tools:$fallbacks,label:$label}' >> "$ctx_file"
tool_resp="$(call_tool_json "$fallback_tool" "$fallback_args")"
tool_text="$(printf '%s' "$tool_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
tool_err="$(extract_tool_error "$tool_text")"
execution_result="$(printf '%s' "$tool_text" | jq -r '
(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $r
| if ($r.success == true) then "success"
elif ($r.success == false) then "error"
elif (($r.blockers|type=="array") and (($r.blockers|length)>0)) then "blocked"
else "unknown" end
')"
jq -nc \
--arg stage "tool_execution" \
--argjson turn "$turn" \
--arg tool "$fallback_tool" \
--argjson args "$fallback_args" \
--arg text "$tool_text" \
--arg result "$execution_result" \
--arg label "$( [[ -n "$tool_err" ]] && echo "bad" || echo "good" )" \
--arg error_type "${tool_err:-}" \
'{stage:$stage,turn:$turn,tool:$tool,args:$args,result:$result,label:$label,error_type:(if ($error_type|length)>0 then $error_type else null end),result_preview:($text|tostring|.[0:2000])}' >> "$ctx_file"
made_tool_call=1
continue
fi
fi
if [[ "$action" == "final" ]]; then
printf '%s\n' "$model_json" | jq . > "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
echo "Task $idx final emitted (native mode)."
return 0
fi
fi
# Deterministic fallback: complete core taskitem flow directly via MCP tools
# when model native-tool output is empty/unusable.
local fb_any=0
local fb_success=0
local fb_tool fb_args fb_resp fb_text fb_err fb_result fb_markdown
local prereq_ops_json
prereq_ops_json="$(printf '%s' "$task_json" | jq -c '.prerequisiteOps // []')"
if has_tool "whetstone_validate_taskitem" && printf '%s' "$prereq_ops_json" | jq -e 'index("validate-intake") != null' >/dev/null 2>&1; then
fb_any=1
fb_tool="whetstone_validate_taskitem"
fb_args="$(preflight_args_for_tool "$fb_tool" "$validate_hint" "$task_json")"
fb_resp="$(call_tool_json "$fb_tool" "$fb_args")"
fb_text="$(printf '%s' "$fb_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
fb_err="$(extract_tool_error "$fb_text")"
fb_result="$(printf '%s' "$fb_text" | jq -r '(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $r | if ($r.success == true) then "success" else "error" end')"
jq -nc --arg stage "tool_execution" --argjson turn "$turn" --arg tool "$fb_tool" --argjson args "$fb_args" --arg text "$fb_text" --arg result "$fb_result" --arg label "$( [[ -n "$fb_err" ]] && echo "bad" || echo "good" )" --arg error_type "${fb_err:-}" '{stage:$stage,turn:$turn,tool:$tool,args:$args,result:$result,label:$label,error_type:(if ($error_type|length)>0 then $error_type else null end),result_preview:($text|tostring|.[0:2000])}' >> "$ctx_file"
[[ -z "$fb_err" ]] && fb_success=1
fi
if has_tool "whetstone_architect_intake" && printf '%s' "$prereq_ops_json" | jq -e 'index("architect-review") != null' >/dev/null 2>&1; then
fb_any=1
fb_tool="whetstone_architect_intake"
fb_markdown="$(build_intake_markdown_from_task "$task_json")"
fb_args="$(jq -nc --arg markdown "$fb_markdown" '{markdown:$markdown}')"
fb_args="$(preflight_args_for_tool "$fb_tool" "$fb_args" "$task_json")"
fb_resp="$(call_tool_json "$fb_tool" "$fb_args")"
fb_text="$(printf '%s' "$fb_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
fb_err="$(extract_tool_error "$fb_text")"
fb_result="$(printf '%s' "$fb_text" | jq -r '(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $r | if ($r.success == true) then "success" else "error" end')"
jq -nc --arg stage "tool_execution" --argjson turn "$turn" --arg tool "$fb_tool" --argjson args "$fb_args" --arg text "$fb_text" --arg result "$fb_result" --arg label "$( [[ -n "$fb_err" ]] && echo "bad" || echo "good" )" --arg error_type "${fb_err:-}" '{stage:$stage,turn:$turn,tool:$tool,args:$args,result:$result,label:$label,error_type:(if ($error_type|length)>0 then $error_type else null end),result_preview:($text|tostring|.[0:2000])}' >> "$ctx_file"
[[ -z "$fb_err" ]] && fb_success=1
fi
if has_tool "whetstone_queue_ready" && printf '%s' "$prereq_ops_json" | jq -e 'index("resolve-dependencies") != null' >/dev/null 2>&1; then
fb_any=1
fb_tool="whetstone_queue_ready"
fb_args="$(preflight_args_for_tool "$fb_tool" '{}' "$task_json")"
fb_resp="$(call_tool_json "$fb_tool" "$fb_args")"
fb_text="$(printf '%s' "$fb_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
fb_err="$(extract_tool_error "$fb_text")"
fb_result="$(printf '%s' "$fb_text" | jq -r '(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $r | if ($r.success == true) then "success" else "error" end')"
jq -nc --arg stage "tool_execution" --argjson turn "$turn" --arg tool "$fb_tool" --argjson args "$fb_args" --arg text "$fb_text" --arg result "$fb_result" --arg label "$( [[ -n "$fb_err" ]] && echo "bad" || echo "good" )" --arg error_type "${fb_err:-}" '{stage:$stage,turn:$turn,tool:$tool,args:$args,result:$result,label:$label,error_type:(if ($error_type|length)>0 then $error_type else null end),result_preview:($text|tostring|.[0:2000])}' >> "$ctx_file"
[[ -z "$fb_err" ]] && fb_success=1
fi
if [[ "$fb_any" -eq 1 && "$fb_success" -eq 1 ]]; then
jq -nc --arg status "done" --arg summary "completed via deterministic MCP fallback after empty native tool output" '{action:"final",status:$status,summary:$summary,next_steps:["continue_with_next_task"]}' > "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
return 0
fi
jq -nc --arg status "blocked" --arg summary "native mode returned no tool calls/final and deterministic fallback did not succeed" \
'{action:"final",status:$status,summary:$summary,next_steps:["handoff_to_model:'"$SMART_MODEL_TAG"'"]}' \
> "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
emit_failure_debug_probes "native_no_tool_or_final" ""
return 1
fi
selection_prompt=$(cat <<EOF
You are a small execution model in a tool loop.
Your goal is to choose the next MCP tool for this Whetstone taskitem.
TASKITEM:
$task_json
AVAILABLE_TOOLS (name-only):
$(printf '%s\n' "$tool_names")
TOOL_SCHEMAS (use these required fields):
$tool_schema_summary
PRIOR_TOOL_LOG:
$context_blob
Respond with STRICT JSON only (no markdown), one object:
1) Tool selection:
{"action":"select_tool","best_tool":"<tool_name>","arguments":{...},"reason":"short reason","confidence":0.0-1.0,"fallback_tools":["tool_a","tool_b"],"intent":"short intent","task_phase":"intake|planning|queue|execution|validation"}
2) Final:
{"action":"final","status":"done|blocked","summary":"what happened","next_steps":["..."]}
Rules:
- Never invent tool names.
- Keep arguments minimal and valid JSON.
- Honor required fields from TOOL_SCHEMAS.
- Do not repeat the same successful tool call with identical arguments; choose the next workflow step instead.
- $prereq_hint
- Do not call whetstone_architect_intake unless markdown has explicit sections: ## Goals, ## Constraints, ## Dependencies, ## Acceptance Criteria.
- Example valid args for whetstone_validate_taskitem:
$validate_hint
- If blocked, return action=final with status=blocked.
EOF
)
gen_json="$(jq -nc --arg model "$MODEL" --arg prompt "$selection_prompt" --arg keep_alive "$KEEP_ALIVE" '
{
model: $model,
prompt: $prompt,
stream: false,
keep_alive: $keep_alive,
options: {
temperature: 0.1,
top_p: 0.9,
num_predict: 1024
}
}'
)"
gen_resp="$(curl -s --max-time "$CURL_TIMEOUT_SECONDS" -X POST "$OLLAMA_URL" -H "Content-Type: application/json" -d "$gen_json" 2>/dev/null || true)"
raw="$(printf '%s' "$gen_resp" | jq -r '.response // ""')"
prompt_tokens="$(printf '%s' "$gen_resp" | jq -r '.prompt_eval_count // 0')"
completion_tokens="$(printf '%s' "$gen_resp" | jq -r '.eval_count // 0')"
total_tokens=$((prompt_tokens + completion_tokens))
if [[ "$CLEAR_CACHE" == "1" ]]; then
ollama stop "$MODEL" >/dev/null 2>&1 || true
fi
model_json="$(printf '%s' "$raw" | extract_first_json)"
action="$(printf '%s' "$model_json" | jq -r '.action // ""')"
{
echo "--- task=$idx turn=$turn ---"
echo "selection_raw:"
printf '%s\n' "$raw"
echo "token_usage: prompt=${prompt_tokens} completion=${completion_tokens} total=${total_tokens} stage=selection"
echo "memory_mode: $mode"
echo "selection_json:"
printf '%s\n' "$model_json" | jq .
} >> "$transcript"
if [[ "$action" == "select_tool" || "$action" == "tool_call" ]]; then
tool="$(printf '%s' "$model_json" | jq -r '.best_tool // .tool // ""')"
args="$(printf '%s' "$model_json" | jq -c '.arguments // {}')"
selection_reason="$(printf '%s' "$model_json" | jq -r '.reason // ""')"
selection_confidence="$(printf '%s' "$model_json" | jq -r '(.confidence // 0) | tonumber? // 0')"
selection_fallbacks="$(printf '%s' "$model_json" | jq -c '.fallback_tools // []')"
selection_intent="$(printf '%s' "$model_json" | jq -r '.intent // ""')"
selection_phase="$(printf '%s' "$model_json" | jq -r '.task_phase // "unknown"')"
selection_label="good"
args="$(normalize_args_for_tool "$tool" "$args")"
args="$(preflight_args_for_tool "$tool" "$args" "$task_json")"
if ! grep -qx "$tool" <<< "$tool_names"; then
selection_label="bad"
jq -nc \
--arg stage "tool_selection" \
--argjson turn "$turn" \
--arg intent "$selection_intent" \
--arg phase "$selection_phase" \
--argjson candidates "$tool_names_json" \
--arg chosen_tool "$tool" \
--argjson args "$args" \
--arg reason "$selection_reason" \
--argjson confidence "$selection_confidence" \
--argjson fallbacks "$selection_fallbacks" \
--arg label "$selection_label" \
--arg error_type "unknown_tool" \
'{
stage:$stage,
turn:$turn,
intent:$intent,
task_phase:$phase,
candidate_tools:$candidates,
chosen_tool:$chosen_tool,
args:$args,
reason:$reason,
confidence:$confidence,
fallback_tools:$fallbacks,
label:$label,
error_type:$error_type
}' >> "$ctx_file"
update_cycle_signature "$turn" "selection:unknown_tool:${tool}" "tool_selection" "$tool" "unknown_tool"
emit_failure_debug_probes "unknown_tool" "$tool"
if [[ "$cycle_escalated" -eq 1 ]]; then
jq -nc --arg status "blocked" --arg summary "escalated to smarter model after cyclical unknown_tool failures" \
--arg model "$SMART_MODEL_TAG" \
'{action:"final",status:$status,summary:$summary,next_steps:["handoff_to_model:" + $model]}' \
> "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
echo "Task $idx escalated due to failure cycle."
return 1
fi
continue
fi
invalid_action_streak=0
jq -nc \
--arg stage "tool_selection" \
--argjson turn "$turn" \
--arg intent "$selection_intent" \
--arg phase "$selection_phase" \
--argjson candidates "$tool_names_json" \
--arg chosen_tool "$tool" \
--argjson args "$args" \
--arg reason "$selection_reason" \
--argjson confidence "$selection_confidence" \
--argjson fallbacks "$selection_fallbacks" \
--arg label "$selection_label" \
'{
stage:$stage,
turn:$turn,
intent:$intent,
task_phase:$phase,
candidate_tools:$candidates,
chosen_tool:$chosen_tool,
args:$args,
reason:$reason,
confidence:$confidence,
fallback_tools:$fallbacks,
label:$label
}' >> "$ctx_file"
tool_resp="$(call_tool_json "$tool" "$args")"
tool_text="$(printf '%s' "$tool_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
tool_err="$(extract_tool_error "$tool_text")"
tool_issue="$(extract_tool_issue "$tool_text")"
made_tool_call=1
# One-shot adapter-level repair retries for known failures.
if [[ "$tool" == "whetstone_validate_taskitem" && "$tool_err" == "task_id_missing" ]]; then
args="$(preflight_args_for_tool "$tool" "$args" "$task_json")"
tool_resp="$(call_tool_json "$tool" "$args")"
tool_text="$(printf '%s' "$tool_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
tool_err="$(extract_tool_error "$tool_text")"
tool_issue="$(extract_tool_issue "$tool_text")"
fi
if [[ "$tool" == "whetstone_architect_intake" && "$tool_err" == "no_sections_found" ]]; then
args="$(preflight_args_for_tool "$tool" '{}' "$task_json")"
tool_resp="$(call_tool_json "$tool" "$args")"
tool_text="$(printf '%s' "$tool_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
tool_err="$(extract_tool_error "$tool_text")"
tool_issue="$(extract_tool_issue "$tool_text")"
fi
if [[ "$tool" == "whetstone_queue_ready" && "$tool_err" == "task_entry_invalid" ]]; then
args="$(preflight_args_for_tool "$tool" "$args" "$task_json")"
tool_resp="$(call_tool_json "$tool" "$args")"
tool_text="$(printf '%s' "$tool_resp" | jq -r '.result.content[0].text // "{}"' 2>/dev/null || echo "{}")"
tool_err="$(extract_tool_error "$tool_text")"
tool_issue="$(extract_tool_issue "$tool_text")"
fi
{
echo "tool_call: $tool $args"
echo "tool_raw:"
printf '%s\n' "$tool_resp"
} >> "$transcript"
execution_result="$(printf '%s' "$tool_text" | jq -r '
(. as $in | if (type=="string") then (fromjson? // {}) else . end) as $r
| if ($r.success == true) then
"success"
elif ($r.success == false) then
"error"
elif (($r.blockers|type=="array") and (($r.blockers|length) > 0)) then
"blocked"
else
"unknown"
end
')"
jq -nc \
--arg stage "tool_execution" \
--argjson turn "$turn" \
--arg tool "$tool" \
--argjson args "$args" \
--arg text "$tool_text" \
--arg result "$execution_result" \
--arg label "$( [[ -n "$tool_err" ]] && echo "bad" || echo "good" )" \
--arg error_type "${tool_err:-}" \
'{
stage:$stage,
turn:$turn,
tool:$tool,
args:$args,
result:$result,
label:$label,
error_type:(if ($error_type|length)>0 then $error_type else null end),
result_preview:($text|tostring|.[0:2000])
}' >> "$ctx_file"
if [[ -n "$tool_err" ]]; then
update_cycle_signature "$turn" "execution:${tool}:${tool_err}" "tool_execution" "$tool" "$tool_err"
last_success_tool_sig=""
success_tool_repeat_streak=0
else
update_cycle_signature "$turn" "" "tool_execution" "$tool" ""
if [[ "$execution_result" == "success" ]]; then
local success_sig
success_sig="${tool}|${args}"
if [[ "$success_sig" == "$last_success_tool_sig" ]]; then
success_tool_repeat_streak=$((success_tool_repeat_streak + 1))
else
last_success_tool_sig="$success_sig"
success_tool_repeat_streak=1
fi
if (( success_tool_repeat_streak >= MAX_SUCCESS_REPEAT_STREAK )); then
update_cycle_signature "$turn" "execution:repeat_success_no_progress:${tool}" "tool_execution" "$tool" "repeat_success_no_progress"
jq -nc --arg status "blocked" --arg summary "escalated after repeated successful tool call without workflow progress" \
--arg model "$SMART_MODEL_TAG" \
'{action:"final",status:$status,summary:$summary,next_steps:["handoff_to_model:" + $model]}' \
> "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
echo "Task $idx escalated after repeated success/no-progress."
return 1
fi
fi
fi
if [[ "$cycle_escalated" -eq 1 ]]; then
jq -nc --arg status "blocked" --arg summary "escalated to smarter model after cyclical tool failures" \
--arg model "$SMART_MODEL_TAG" \
'{action:"final",status:$status,summary:$summary,next_steps:["handoff_to_model:" + $model]}' \
> "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
echo "Task $idx escalated due to tool failure cycle."
return 1
fi
if [[ "$ADAPTIVE_MEMORY" == "1" ]]; then
if [[ -n "$tool_issue" ]]; then
if [[ "$tool_issue" == "$last_issue_sig" ]]; then
issue_streak=$((issue_streak + 1))
else
last_issue_sig="$tool_issue"
issue_streak=1
fi
else
last_issue_sig=""
issue_streak=0
fi
if (( prompt_tokens >= RESET_PROMPT_TOKENS )); then
force_reset=1
reset_reason="prompt_token_pressure:${prompt_tokens}"
elif (( issue_streak >= RESET_ERROR_STREAK )); then
force_reset=1
reset_reason="repeated_issue:${last_issue_sig}"
emit_failure_debug_probes "repeated_issue" "$last_issue_sig"
fi
fi
continue
fi
if [[ "$action" == "final" ]]; then
if [[ "$(printf '%s' "$model_json" | jq -r '.status // ""')" == "done" && "$made_tool_call" -eq 0 ]]; then
echo "{\"turn\":$turn,\"error\":\"premature_final_done_no_tool_call\"}" >> "$ctx_file"
continue
fi
printf '%s\n' "$model_json" | jq . > "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
echo "Task $idx final emitted."
return 0
fi
if [[ "$action" == "escalate" ]]; then
jq -nc --arg status "blocked" --arg summary "slm requested escalation to smarter model" \
--arg model "$SMART_MODEL_TAG" \
'{action:"final",status:$status,summary:$summary,next_steps:["handoff_to_model:" + $model]}' \
> "$OUT_DIR/task_${idx}_final.json"
jq -nc \
--arg stage "handoff" \
--argjson turn "$turn" \
--arg label "bad" \
--arg reason "explicit_escalation" \
--arg model "$SMART_MODEL_TAG" \
'{stage:$stage,turn:$turn,label:$label,error_type:$reason,handoff_model:$model}' >> "$ctx_file"
emit_failure_debug_probes "escalate_action" "model_requested_handoff"
write_failure_cycle_summary
echo "Task $idx escalated by explicit action."
return 1
fi
jq -nc \
--arg stage "tool_selection" \
--argjson turn "$turn" \
--arg label "bad" \
--arg error_type "invalid_action" \
--arg raw_preview "${raw:0:500}" \
'{stage:$stage,turn:$turn,label:$label,error_type:$error_type,raw_preview:$raw_preview}' >> "$ctx_file"
invalid_action_streak=$((invalid_action_streak + 1))
update_cycle_signature "$turn" "selection:invalid_action" "tool_selection" "" "invalid_action"
emit_failure_debug_probes "invalid_action" "${raw:0:200}"
if (( invalid_action_streak >= MAX_INVALID_ACTION_STREAK )) || [[ "$cycle_escalated" -eq 1 ]]; then
jq -nc --arg status "blocked" --arg summary "escalated after repeated invalid actions" \
--arg model "$SMART_MODEL_TAG" \
'{action:"final",status:$status,summary:$summary,next_steps:["handoff_to_model:" + $model]}' \
> "$OUT_DIR/task_${idx}_final.json"
write_failure_cycle_summary
echo "Task $idx escalated after invalid_action streak."
return 1
fi
done
jq -nc --arg status "blocked" --arg summary "max turns reached without final action" \
'{action:"final",status:$status,summary:$summary,next_steps:["review transcript and rerun with higher max turns"]}' \
> "$OUT_DIR/task_${idx}_final.json"
emit_failure_debug_probes "max_turns_reached" "no_final_action"
write_failure_cycle_summary
echo "Task $idx reached max turns."
return 1
}
fail=0
if [[ -n "$TASK_INDEX" ]]; then
run_one_task "$task_payload" "$TASK_INDEX" || fail=1
else
count="$(jq '.tasks | length' "$TASKITEMS_FILE")"
for i in $(seq 0 $((count - 1))); do
item="$(jq -c ".tasks[$i]" "$TASKITEMS_FILE")"
run_one_task "$item" "$i" || fail=1
done
fi
echo
echo "Output dir: $OUT_DIR"
if [[ "$fail" -ne 0 ]]; then
exit 4
fi