tools: add structured STATUS prefix signals for smaller model clarity
Addresses the loop/no-op problem where small models (e.g. devstral-small) fail to interpret ambiguous prose output as terminal control signals. Changes: - file_write: prefix STATUS=done/no_change/error with STATE_CHANGED and RETRYABLE - file_edit: same prefix scheme; also RETRYABLE=true for ambiguous match errors - file_read: prefix STATUS=ok/empty/not_found/error to distinguish data absence from failure - file_glob: prefix STATUS=ok/no_matches/error with MATCH_COUNT - file_grep: prefix STATUS=ok/no_matches/error with MATCH_COUNT - task_add: emit STATUS=ok ADDED=N TOTAL_TASKS=M - task_check: emit STATUS=done TASK_COMPLETE=true/false REMAINING=N - task_clear: emit STATUS=ok CLEARED=N REMAINING=N TASK_COMPLETE=true/false - cascade: rewrite worker termination block as numbered mandatory rules; add explicit STATUS: error branch; clarify stop-after-write requirement
This commit is contained in:
parent
dd522e48f9
commit
137d2c0e84
|
|
@ -38,11 +38,24 @@ prompt="[parent_session_id=${OLLIE_SESSION_ID}]
|
|||
[session_id=${id}]
|
||||
|
||||
You are a worker agent. Execute the following task exactly. Output only the result.
|
||||
When finished, write your result to: ${OLLIE}/s/${OLLIE_SESSION_ID}/prompt
|
||||
Use this exact format:
|
||||
[from=${id}]
|
||||
STATUS: done
|
||||
SUMMARY: <your result>
|
||||
|
||||
TERMINATION RULES (mandatory — follow exactly):
|
||||
1. Complete the task using whatever tools are needed.
|
||||
2. When finished (success or failure), write your result to: ${OLLIE}/s/${OLLIE_SESSION_ID}/prompt
|
||||
Use file_write with this exact format and nothing after it:
|
||||
|
||||
[from=${id}]
|
||||
STATUS: done
|
||||
SUMMARY: <one-line result or confirmation>
|
||||
|
||||
On failure, use:
|
||||
[from=${id}]
|
||||
STATUS: error
|
||||
REASON: <why the task could not be completed>
|
||||
|
||||
3. After writing the result, STOP. Do not call any more tools. Do not write again.
|
||||
4. Do not re-read the file to verify. Do not retry the write. One write, then stop.
|
||||
5. Do not expand the task scope. If the task is ambiguous or impossible, report STATUS: error immediately.
|
||||
|
||||
TASK:
|
||||
${instruction}"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def format_diff(path, old, new):
|
|||
new_lines = new.splitlines(keepends=True)
|
||||
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
if not diff:
|
||||
return "(no changes)"
|
||||
return None
|
||||
return f"```diff\n{diff}```"
|
||||
|
||||
def find_exact(content, find):
|
||||
|
|
@ -70,13 +70,13 @@ def robust_replace(content, old_string, new_string, replace_all):
|
|||
find_indent_flexible(content, old_string) or
|
||||
find_trimmed_boundary(content, old_string))
|
||||
if match is None:
|
||||
return None, "old_string not found in file"
|
||||
return None, "old_string not found in file", False
|
||||
if replace_all:
|
||||
return content.replace(match, new_string), None
|
||||
return content.replace(match, new_string), None, False
|
||||
n = content.count(match)
|
||||
if n > 1:
|
||||
return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context"
|
||||
return content.replace(match, new_string, 1), None
|
||||
return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context", True
|
||||
return content.replace(match, new_string, 1), None, False
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
print("usage: file_edit <file_path> <old_string> <new_string> [replace_all=true|1|yes]")
|
||||
|
|
@ -89,20 +89,24 @@ _ra = sys.argv[4].lower() if len(sys.argv) > 4 else ''
|
|||
replace_all = _ra.split('=')[-1] in ('true', '1', 'yes')
|
||||
|
||||
if not os.path.isabs(file_path):
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
|
||||
print(f"error: path must be absolute, got: {file_path}")
|
||||
sys.exit(1)
|
||||
if old_string == new_string:
|
||||
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
|
||||
print("error: old_string and new_string must be different")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(file_path):
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
|
||||
print(f"error: file not found: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(file_path, 'r', errors='replace') as f:
|
||||
original = f.read()
|
||||
|
||||
new_content, err = robust_replace(original, old_string, new_string, replace_all)
|
||||
new_content, err, retryable = robust_replace(original, old_string, new_string, replace_all)
|
||||
if err:
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE={'true' if retryable else 'false'}")
|
||||
print(f"error: {err}")
|
||||
sys.exit(1)
|
||||
|
||||
|
|
@ -110,7 +114,14 @@ try:
|
|||
with open(file_path, 'w') as f:
|
||||
f.write(new_content)
|
||||
except OSError as e:
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
|
||||
print(f"error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print(format_diff(file_path, original, new_content))
|
||||
diff = format_diff(file_path, original, new_content)
|
||||
if diff is None:
|
||||
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
|
||||
print("(no changes)")
|
||||
else:
|
||||
print("STATUS=done STATE_CHANGED=true")
|
||||
print(diff)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ search_path="${2:-$PWD}"
|
|||
[ -z "$pattern" ] && echo "usage: file_glob <pattern> [path]" && exit 1
|
||||
|
||||
if [ ! -d "$search_path" ]; then
|
||||
echo "STATUS=error"
|
||||
echo "error: path is not a directory: $search_path"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -17,14 +18,21 @@ fi
|
|||
rg_out=$(rg --no-config --files -g "$pattern" "$search_path" 2>&1)
|
||||
rg_exit=$?
|
||||
if [ $rg_exit -gt 1 ]; then
|
||||
echo "STATUS=error"
|
||||
echo "error: $rg_out"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$rg_out" ]; then
|
||||
echo "STATUS=no_matches MATCH_COUNT=0"
|
||||
echo "no matches"
|
||||
exit 0
|
||||
fi
|
||||
echo "$rg_out" \
|
||||
|
||||
sorted=$(echo "$rg_out" \
|
||||
| xargs -d '\n' stat --printf '%Y\t%n\n' 2>&1 \
|
||||
| sort -rn \
|
||||
| cut -f2-
|
||||
| cut -f2-)
|
||||
|
||||
count=$(echo "$sorted" | wc -l | tr -d ' ')
|
||||
echo "STATUS=ok MATCH_COUNT=$count"
|
||||
echo "$sorted"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ parser.add_argument('--offset', type=int, default=0)
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
rg = 'rg'
|
||||
search_path = args.path or os.getcwd()
|
||||
|
||||
cmd = ['rg', '--no-config', '--color=never']
|
||||
|
|
@ -58,9 +57,11 @@ cmd += ['-e', args.pattern, search_path]
|
|||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 1:
|
||||
print("STATUS=no_matches MATCH_COUNT=0")
|
||||
print("no matches")
|
||||
sys.exit(0)
|
||||
if result.returncode > 1:
|
||||
print(f"STATUS=error")
|
||||
print(f"error: {result.stderr.strip() or f'rg exited with {result.returncode}'}")
|
||||
sys.exit(1)
|
||||
|
||||
|
|
@ -81,7 +82,6 @@ if args.mode == 'content':
|
|||
if line == '--':
|
||||
formatted.append(line)
|
||||
continue
|
||||
# file:lineno:content or file-lineno-content (context lines)
|
||||
for sep in (':', '-'):
|
||||
parts = line.split(sep, 2)
|
||||
if len(parts) == 3 and parts[1].isdigit():
|
||||
|
|
@ -91,6 +91,7 @@ if args.mode == 'content':
|
|||
formatted.append(line)
|
||||
lines = formatted
|
||||
|
||||
print(f"STATUS=ok MATCH_COUNT={total}")
|
||||
print('\n'.join(lines))
|
||||
if capped:
|
||||
print(f"\n(showing {max_matches} of {total} matches; use --head=N or --offset=N for more)")
|
||||
|
|
|
|||
|
|
@ -30,19 +30,23 @@ start = int(args[1]) if len(args) > 1 else 1
|
|||
end = int(args[2]) if len(args) > 2 else None
|
||||
|
||||
if not os.path.isabs(file_path):
|
||||
print(f"STATUS=error")
|
||||
print(f"error: path must be absolute, got: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print("STATUS=not_found")
|
||||
print("(new file)")
|
||||
sys.exit(0)
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"STATUS=error")
|
||||
print(f"error: not a regular file: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
if b'\x00' in f.read(8192):
|
||||
print("STATUS=ok BINARY=true")
|
||||
print("(binary file)")
|
||||
sys.exit(0)
|
||||
|
||||
|
|
@ -55,14 +59,17 @@ else:
|
|||
end = min(end, start + MAX_SEGMENT - 1)
|
||||
|
||||
with open(file_path, 'r', errors='replace') as f:
|
||||
# Skip lines before start
|
||||
for _ in islice(f, start - 1):
|
||||
pass
|
||||
# Read only the lines we need
|
||||
segment_lines = list(islice(f, end - start + 1))
|
||||
|
||||
if not segment_lines:
|
||||
print("(empty file)" if start == 1 else "(no lines in range)")
|
||||
if start == 1:
|
||||
print("STATUS=ok EMPTY=true")
|
||||
print("(empty file)")
|
||||
else:
|
||||
print("STATUS=ok EMPTY=true")
|
||||
print("(no lines in range)")
|
||||
sys.exit(0)
|
||||
|
||||
out = []
|
||||
|
|
@ -73,4 +80,5 @@ for i, line in enumerate(segment_lines):
|
|||
text = text[:MAX_LINE_LEN] + '...'
|
||||
out.append(f"{lineno:>6}\t{text}")
|
||||
|
||||
print(f"STATUS=ok LINES={len(out)}")
|
||||
print('\n'.join(out))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def format_diff(path, old, new):
|
|||
new_lines = new.splitlines(keepends=True)
|
||||
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
if not diff:
|
||||
return "(no changes)"
|
||||
return None
|
||||
return f"```diff\n{diff}```"
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
|
|
@ -22,11 +22,13 @@ file_path = sys.argv[1]
|
|||
content = sys.argv[2]
|
||||
|
||||
if not os.path.isabs(file_path):
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
|
||||
print(f"error: path must be absolute, got: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
parent = os.path.dirname(file_path)
|
||||
if parent and not os.path.isdir(parent):
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
|
||||
print(f"error: parent directory does not exist: {parent}")
|
||||
sys.exit(1)
|
||||
|
||||
|
|
@ -39,7 +41,14 @@ try:
|
|||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
except OSError as e:
|
||||
print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false")
|
||||
print(f"error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print(format_diff(file_path, old_content, content))
|
||||
diff = format_diff(file_path, old_content, content)
|
||||
if diff is None:
|
||||
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
|
||||
print("(no changes)")
|
||||
else:
|
||||
print("STATUS=done STATE_CHANGED=true")
|
||||
print(diff)
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ added=0
|
|||
done
|
||||
} > "$plan"
|
||||
|
||||
echo "added $added task(s)"
|
||||
total=$(grep -c '^- \[' "$plan" 2>/dev/null || echo 0)
|
||||
echo "STATUS=ok ADDED=$added TOTAL_TASKS=$total"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ lines = content.splitlines(keepends=True)
|
|||
tasks = [(i, l) for i, l in enumerate(lines) if l.startswith("- [ ]") or l.startswith("- [x]")]
|
||||
|
||||
if not tasks:
|
||||
print("STATUS=done TASK_COMPLETE=true REMAINING=0")
|
||||
print("no tasks in plan")
|
||||
sys.exit(0)
|
||||
|
||||
|
|
@ -32,6 +33,8 @@ if n is None:
|
|||
# First unchecked
|
||||
target = next(((i, l) for i, l in tasks if l.startswith("- [ ]")), None)
|
||||
if target is None:
|
||||
remaining = 0
|
||||
print(f"STATUS=done TASK_COMPLETE=true REMAINING=0")
|
||||
print("all tasks already checked")
|
||||
sys.exit(0)
|
||||
else:
|
||||
|
|
@ -43,4 +46,8 @@ else:
|
|||
idx, line = target
|
||||
lines[idx] = line.replace("- [ ]", "- [x]", 1)
|
||||
open(plan_path, "w").write("".join(lines))
|
||||
|
||||
remaining = sum(1 for _, l in tasks if l.startswith("- [ ]") and _ != idx)
|
||||
task_complete = remaining == 0
|
||||
print(f"STATUS=done TASK_COMPLETE={'true' if task_complete else 'false'} REMAINING={remaining}")
|
||||
print(f"checked: {lines[idx].rstrip()}")
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@
|
|||
OLLIE="${OLLIE:-$HOME/mnt/ollie}"
|
||||
plan="$OLLIE/s/$OLLIE_SESSION_ID/plan"
|
||||
|
||||
[ ! -f "$plan" ] && echo "no plan found" && exit 0
|
||||
[ ! -f "$plan" ] && echo "STATUS=ok CLEARED=0 REMAINING=0 TASK_COMPLETE=true" && echo "no plan found" && exit 0
|
||||
|
||||
before=$(grep -c '^\- \[.\]' "$plan" 2>/dev/null || echo 0)
|
||||
grep -v '^\- \[x\]' "$plan" > "$plan.tmp" && mv "$plan.tmp" "$plan"
|
||||
after=$(grep -c '^\- \[.\]' "$plan" 2>/dev/null || echo 0)
|
||||
|
||||
removed=$(( before - after ))
|
||||
task_complete="false"
|
||||
[ "$after" -eq 0 ] && task_complete="true"
|
||||
|
||||
echo "STATUS=ok CLEARED=$removed REMAINING=$after TASK_COMPLETE=$task_complete"
|
||||
echo "cleared $removed completed task(s), $after remaining"
|
||||
|
|
|
|||
Loading…
Reference in New Issue