1273 lines
49 KiB
Bash
Executable File
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
|