cp_cli lives in agent-runtimes; previously every planner call ran 'python3 -m lib.cp_cli' from the current repo with no --context, so the script only worked inside agent-runtimes. The context now resolves from ~/.cpconfig by matching project_id to the repo directory name. Verified live from hatchmate (item + parent + documents fetched via the hatchmate context). Claude-Session: https://claude.ai/code/session_01JgJbWzVJioVznVTFyQFEfx
7.2 KiB
7.2 KiB
bg-model-call
Purpose
Submit a prompt to a third-party model via the agent-runtimes control plane, wait for completion, and write the result to a file — enabling the ScheduleWakeup polling pattern for second-opinion consultations without blocking the main Claude Code session.
Usage
bg-model-call [OPTIONS]
Required flags
| Flag | Description |
|---|---|
--model <name> |
Model alias: minimax, sonnet, haiku, opus |
--prompt <file> |
Path to a file containing the prompt text |
--output <file> |
Path where the result will be written |
Optional flags
| Flag | Default | Description |
|---|---|---|
--template <name> |
(derived from --model) |
Override the task template name (without .yaml). Takes precedence over the --model → template mapping. |
--project <id> |
bg-model-call |
Project ID sent to the CP, used for task grouping in agent-monitor |
--url <cp-url> |
$CP_URL, then http://localhost:8100 |
Control plane base URL |
--task-name <name> |
bg-model-call-<model> |
Short task name shown in agent-monitor |
--timeout <seconds> |
1800 |
Max seconds to wait for the task before declaring it failed |
--poll-interval <seconds> |
10 |
Seconds between CP state polls |
--dryrun, -n |
— | Resolve all values and print what would be submitted; do not call the CP or write any files |
--help, -h |
— | Show usage information and exit 0 |
Model-to-Template Mapping
A constant defined at the top of the script:
| Model alias | Template name |
|---|---|
minimax |
minimax-planning |
sonnet |
sonnet-code-repo |
haiku |
haiku-code-repo |
opus |
opus-code-repo |
The --template flag overrides this mapping entirely.
Behaviour
- Parse and validate all flags. Exit 1 with an error message if required flags are missing or values are invalid.
- Resolve the CP URL:
--urlflag →$CP_URLenv var →http://localhost:8100. - Resolve the template name:
--templateflag if given, otherwise look up--modelin the mapping table. If the model alias is not in the table, exit 1. - Read the prompt text from the
--promptfile. If the file is not readable, exit 1. - Locate the
dispatch-taskCLI:- Check if
dispatch-taskis on PATH. - Otherwise try
<script-dir>/../../../projects/agent-runtimes/scripts/dispatch-taskas a fallback (for dev environments). - If neither is found, fall back to direct
curlPOST to$CP_URL/tasks.
- Check if
- Submit the task to the CP via the resolved
dispatch-task(or curl fallback):--name <task-name>--project-id <project>--prompt-file <prompt-file>--template <template>--cp-url <cp-url>--format json(to capture the task_id from stdout)- The
dispatch-tasktool usesCP_AUTH_TOKENfrom the environment if present.
- Extract the
task_idfrom the submission response. - Write a
.bg-model-call-<task-id>tracking file in the same directory as--output(content: task ID, model, template, timestamp). This avoids collisions between concurrent calls. - Poll the CP using
wait-for-tasks(or thedispatch-task --waitpattern):- Locate
wait-for-tasksusing the same PATH + fallback logic asdispatch-task. - If not found, poll manually via
curl GET $CP_URL/tasks/<task-id>in a sleep loop. - Poll every
--poll-intervalseconds up to--timeoutseconds total.
- Locate
- Once the task reaches a terminal state:
- Succeeded: Fetch logs via
GET $CP_URL/tasks/<task-id>/logs. Write the log content to--output. Write<output>.donesentinel (content:task_id=<id>+ newline +completed_at=<timestamp>). Remove the tracking file. - Failed / timed_out / cancelled: Write
<output>.failedsentinel (content:task_id=<id>,state=<state>,error=<detail if available>). Remove the tracking file. Exit 1.
- Succeeded: Fetch logs via
- Exit 0 on success.
Output Files
| File | When written | Content |
|---|---|---|
<output> |
On success | Raw text from GET /tasks/<id>/logs |
<output>.done |
On success | task_id=<id>, completed_at=<timestamp> |
<output>.failed |
On failure/timeout | task_id=<id>, state=<state>, error=<detail> |
.bg-model-call-<task-id> |
After submission | task_id=<id>, model=<alias>, template=<name>, submitted_at=<timestamp> |
The .done sentinel is the signal the caller polls for. The caller reads <output> once .done appears.
Dryrun Behaviour
When --dryrun or -n is passed, the script:
- Resolves all values (CP URL, template, task name, project ID).
- Reads (and validates) the prompt file.
- Prints a summary of what would be submitted — no CP calls, no file writes.
Example output:
[dryrun] bg-model-call — no changes will be made
CP URL: http://localhost:8100
Model: minimax
Template: minimax-planning
Task name: bg-model-call-minimax
Project ID: bg-model-call
Prompt file: /tmp/my-prompt.txt
Prompt size: 142 bytes
Output file: /tmp/result.txt
Sentinel: /tmp/result.txt.done
dispatch-task: /home/user/dev/claude/projects/agent-runtimes/scripts/dispatch-task
Edge Cases
| Case | Handling |
|---|---|
--model missing and --template missing |
Error: --model is required (or use --template to specify a template directly), exit 1 |
--prompt missing |
Error: --prompt is required, exit 1 |
--output missing |
Error: --output is required, exit 1 |
--model not in mapping table |
Error: Unknown model alias '<name>'. Valid values: minimax, sonnet, haiku, opus, exit 1 |
| Prompt file not readable | Error: Cannot read prompt file: <path>, exit 1 |
--timeout not a positive integer |
Error: --timeout must be a positive integer, exit 1 |
--poll-interval not a positive integer |
Error: --poll-interval must be a positive integer, exit 1 |
| CP submission fails | Write <output>.failed, exit 1 |
Task times out (wait exceeds --timeout) |
Write <output>.failed with state=timed_out, exit 1 |
| Output directory does not exist | Error: Output directory does not exist: <dir>, exit 1 |
Security
- No credentials in the script. CP authentication (if required) is handled by
dispatch-taskreadingCP_AUTH_TOKENfrom the environment. Thebg-model-callscript only passes--cp-urland never handles tokens directly. - Prompt content is passed via file (
--prompt-file), never as a command-line argument. This avoids the prompt appearing inpsoutput.
Examples
# Submit to MiniMax and poll in the background; check for result from a wakeup
echo "Review this spec for security issues: $(cat spec/auth.md)" > /tmp/minimax-prompt.txt
bg-model-call \
--model minimax \
--prompt /tmp/minimax-prompt.txt \
--output /tmp/minimax-result.txt \
--project my-project &
# Later (from ScheduleWakeup), check for completion:
if [[ -f /tmp/minimax-result.txt.done ]]; then
cat /tmp/minimax-result.txt
fi
# Dryrun to inspect resolved values
bg-model-call \
--model sonnet \
--prompt /tmp/my-prompt.txt \
--output /tmp/result.txt \
--dryrun
# Use a specific template instead of the model mapping
bg-model-call \
--template minimax-planning \
--prompt /tmp/prompt.txt \
--output /tmp/result.txt \
--dryrun