docs: Added common pitfalls docs for custom bash scripts
+111
@@ -336,6 +336,117 @@ argc_some_field="$(jq -r '.some_field' <<< "$LLM_AGENT_RAW_JSON")"
|
||||
|
||||
---
|
||||
|
||||
# Output Handling and Common Pitfalls
|
||||
|
||||
Coyote captures your tool's result from the `$LLM_OUTPUT` file (not from stdout), and it applies a few rules that are
|
||||
easy to trip over. The bundled `fs_*` tools follow the patterns below; your own tools should too.
|
||||
|
||||
## An empty result is shown to the model as `"DONE"`
|
||||
|
||||
When a tool exits `0` but writes **nothing** to `$LLM_OUTPUT`, Coyote has no content to return, so the model receives
|
||||
the bare string `"DONE"`. The model cannot distinguish that from a broken tool. It has no idea whether the file was
|
||||
empty, the search had no hits, or the script silently failed.
|
||||
|
||||
Always write *something* for every successful outcome, including the "nothing found" cases:
|
||||
|
||||
```bash
|
||||
# BAD: a search that finds nothing writes nothing, so the model just sees "DONE"
|
||||
main() {
|
||||
grep -rn "$argc_pattern" . >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# GOOD: empty results get an explicit, informative message
|
||||
main() {
|
||||
local results
|
||||
results=$(grep -rn "$argc_pattern" . 2>/dev/null) || true
|
||||
if [[ -z "$results" ]]; then
|
||||
echo "No matches found for: $argc_pattern" >> "$LLM_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
echo "$results" >> "$LLM_OUTPUT"
|
||||
}
|
||||
```
|
||||
|
||||
The same applies to reading an empty file, listing an empty directory, or an offset/range that lands past the end of
|
||||
the data. If a "no result" is a normal outcome, say so explicitly.
|
||||
|
||||
## Errors written to `$LLM_OUTPUT` are discarded on a non-zero exit
|
||||
|
||||
Coyote reads `$LLM_OUTPUT` **only when the tool exits `0`**. On a non-zero exit it instead returns
|
||||
`Tool call '<name>' exited with code N` plus whatever the tool printed to **stdout/stderr**. The `$LLM_OUTPUT` file is
|
||||
ignored. So the following hides your helpful message; the model only sees `exited with code 1`:
|
||||
|
||||
```bash
|
||||
# BAD: the message lands in $LLM_OUTPUT, then the non-zero exit makes Coyote ignore it
|
||||
main() {
|
||||
if [[ ! -d "$argc_path" ]]; then
|
||||
echo "Error: directory not found: $argc_path" >> "$LLM_OUTPUT"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
Use one of these instead:
|
||||
|
||||
```bash
|
||||
# OPTION A (real errors): print to stderr, then exit non-zero.
|
||||
# Coyote captures stderr and attaches it to the error the model sees.
|
||||
main() {
|
||||
if [[ ! -d "$argc_path" ]]; then
|
||||
echo "Error: directory not found: $argc_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# OPTION B (expected "empty"/"not found" outcomes that are not real failures):
|
||||
# write the note to $LLM_OUTPUT and exit 0 so Coyote returns it as normal content.
|
||||
main() {
|
||||
if [[ ! -d "$argc_path" ]]; then
|
||||
echo "No such directory: $argc_path" >> "$LLM_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Preserve a file's final line when it has no trailing newline
|
||||
|
||||
Two common idioms silently drop the last line of a file that does not end in `\n`:
|
||||
|
||||
- `while IFS= read -r line; do ...; done` stops before the final unterminated line, because `read` returns non-zero on it.
|
||||
- `wc -l` counts *newlines*, so it under-reports the line count by one for such files.
|
||||
|
||||
```bash
|
||||
# BAD: drops the final line when the file ends without a newline; total is off by one
|
||||
total=$(wc -l < "$file")
|
||||
while IFS= read -r line; do
|
||||
printf '%s\n' "$line"
|
||||
done < "$file"
|
||||
|
||||
# GOOD: `|| [[ -n "$line" ]]` processes the final unterminated line; awk counts it correctly
|
||||
total=$(awk 'END { print NR }' "$file")
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
printf '%s\n' "$line"
|
||||
done < "$file"
|
||||
```
|
||||
|
||||
## Type numeric options with `<INT>` / `<NUM>`
|
||||
|
||||
An `@option` with no value notation compiles to a **string** in the JSON schema, so the model tends to send `"359"`
|
||||
instead of `359`. Add an `<INT>` (integer) or `<NUM>` (floating point) notation so the schema advertises the right type:
|
||||
|
||||
```bash
|
||||
# BAD: schema type is "string"; the model sends "359"
|
||||
# @option --offset The line to start from
|
||||
|
||||
# GOOD: schema type is "integer"; the model sends 359
|
||||
# @option --offset <INT> The line to start from
|
||||
```
|
||||
|
||||
Your script still receives the value as text in `argc_offset` either way (Bash has no typed variables), but the schema
|
||||
now guides the model and lets strict providers validate the call.
|
||||
|
||||
---
|
||||
|
||||
# Prompt Helpers
|
||||
It's often useful to create interactive prompts for our bash tools so that our tools can get input from
|
||||
users.
|
||||
|
||||
Reference in New Issue
Block a user