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
|
# Prompt Helpers
|
||||||
It's often useful to create interactive prompts for our bash tools so that our tools can get input from
|
It's often useful to create interactive prompts for our bash tools so that our tools can get input from
|
||||||
users.
|
users.
|
||||||
|
|||||||
Reference in New Issue
Block a user