fix: feat: hire-an-agent should support local models (--local-model flag) (#521)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aeaef880ec
commit
1e4754675d
2 changed files with 113 additions and 80 deletions
|
|
@ -51,7 +51,7 @@ Usage:
|
|||
disinto ci-logs <pipeline> [--step <name>]
|
||||
Read CI logs from Woodpecker SQLite
|
||||
disinto release <version> Create vault PR for release (e.g., v1.2.0)
|
||||
disinto hire-an-agent <agent-name> <role> [--formula <path>]
|
||||
disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>]
|
||||
Hire a new agent (create user + .profile repo)
|
||||
|
||||
Init options:
|
||||
|
|
@ -64,6 +64,9 @@ Init options:
|
|||
|
||||
Hire an agent options:
|
||||
--formula <path> Path to role formula TOML (default: formulas/<role>.toml)
|
||||
--local-model <url> Base URL for local model server (e.g., http://10.10.10.1:8081)
|
||||
--model <name> Model name for local model (e.g., unsloth/Qwen3.5-35B-A3B)
|
||||
--poll-interval <s> Poll interval in seconds (default: 60)
|
||||
|
||||
CI logs options:
|
||||
--step <name> Filter logs to a specific step (e.g., smoke-init)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
#
|
||||
# Usage:
|
||||
# source "${FACTORY_ROOT}/lib/hire-agent.sh"
|
||||
# disinto_hire_an_agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--poll-interval <seconds>]
|
||||
# disinto_hire_an_agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>] [--poll-interval <seconds>]
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -22,11 +22,12 @@ disinto_hire_an_agent() {
|
|||
local role="${2:-}"
|
||||
local formula_path=""
|
||||
local local_model=""
|
||||
local model_name=""
|
||||
local poll_interval=""
|
||||
|
||||
if [ -z "$agent_name" ] || [ -z "$role" ]; then
|
||||
echo "Error: agent-name and role required" >&2
|
||||
echo "Usage: disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--poll-interval <seconds>]" >&2
|
||||
echo "Usage: disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>] [--poll-interval <seconds>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift 2
|
||||
|
|
@ -42,6 +43,10 @@ disinto_hire_an_agent() {
|
|||
local_model="$2"
|
||||
shift 2
|
||||
;;
|
||||
--model)
|
||||
model_name="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
poll_interval="$2"
|
||||
shift 2
|
||||
|
|
@ -71,7 +76,8 @@ disinto_hire_an_agent() {
|
|||
echo "Formula: ${formula_path}"
|
||||
if [ -n "$local_model" ]; then
|
||||
echo "Local model: ${local_model}"
|
||||
echo "Poll interval: ${poll_interval:-300}s"
|
||||
echo "Model name: ${model_name:-local-model}"
|
||||
echo "Poll interval: ${poll_interval:-60}s"
|
||||
fi
|
||||
|
||||
# Ensure FORGE_TOKEN is set
|
||||
|
|
@ -367,11 +373,6 @@ EOF
|
|||
echo ""
|
||||
echo "Step 6: Configuring local model agent..."
|
||||
|
||||
local override_file="${FACTORY_ROOT}/docker-compose.override.yml"
|
||||
local override_dir
|
||||
override_dir=$(dirname "$override_file")
|
||||
mkdir -p "$override_dir"
|
||||
|
||||
# Validate model endpoint is reachable
|
||||
echo " Validating model endpoint: ${local_model}"
|
||||
if ! curl -sf --max-time 10 "${local_model}/health" >/dev/null 2>&1; then
|
||||
|
|
@ -384,83 +385,112 @@ EOF
|
|||
echo " Model endpoint is reachable"
|
||||
fi
|
||||
|
||||
# Generate service name from agent name (lowercase)
|
||||
local service_name="agents-${agent_name}"
|
||||
service_name=$(echo "$service_name" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Set default poll interval
|
||||
local interval="${poll_interval:-300}"
|
||||
|
||||
# Generate the override compose file
|
||||
# Bash expands ${service_name}, ${local_model}, ${interval}, ${PROJECT_NAME} at generation time
|
||||
# \$HOME, \$FORGE_TOKEN become ${HOME}, ${FORGE_TOKEN} in the file for docker-compose runtime expansion
|
||||
cat > "$override_file" <<OVERRIDEOF
|
||||
# docker-compose.override.yml — auto-generated by disinto hire-an-agent
|
||||
# Local model agent configuration for ${agent_name}
|
||||
|
||||
services:
|
||||
${service_name}:
|
||||
image: disinto-agents:latest
|
||||
profiles: ["local-model"]
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- apparmor=unconfined
|
||||
volumes:
|
||||
- agent-data-llama:/home/agent/data
|
||||
- project-repos-llama:/home/agent/repos
|
||||
- \$HOME/.claude:/home/agent/.claude
|
||||
- \$HOME/.claude.json:/home/agent/.claude.json:ro
|
||||
- CLAUDE_BIN_PLACEHOLDER:/usr/local/bin/claude:ro
|
||||
- \$HOME/.ssh:/home/agent/.ssh:ro
|
||||
- \$HOME/.config/sops/age:/home/agent/.config/sops/age:ro
|
||||
environment:
|
||||
FORGE_URL: http://forgejo:3000
|
||||
FORGE_TOKEN: ${FORGE_TOKEN_DEVQWEN:-}
|
||||
FORGE_SUPERVISOR_TOKEN: ${FORGE_SUPERVISOR_TOKEN:-}
|
||||
FORGE_PREDICTOR_TOKEN: ${FORGE_PREDICTOR_TOKEN:-}
|
||||
FORGE_ARCHITECT_TOKEN: ${FORGE_ARCHITECT_TOKEN:-}
|
||||
FORGE_VAULT_TOKEN: ${FORGE_VAULT_TOKEN:-}
|
||||
FORGE_PLANNER_TOKEN: ${FORGE_PLANNER_TOKEN:-}
|
||||
FORGE_BOT_USERNAMES: ${FORGE_BOT_USERNAMES:-}
|
||||
WOODPECKER_TOKEN: ${WOODPECKER_TOKEN:-}
|
||||
CLAUDE_TIMEOUT: ${CLAUDE_TIMEOUT:-7200}
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: ${CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:-1}
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
ANTHROPIC_BASE_URL: ${local_model}
|
||||
FORGE_ADMIN_PASS: ${FORGE_ADMIN_PASS:-}
|
||||
DISINTO_CONTAINER: "1"
|
||||
PROJECT_REPO_ROOT: /home/agent/repos/${PROJECT_NAME:-project}
|
||||
WOODPECKER_DATA_DIR: /woodpecker-data
|
||||
AGENT_ROLES: dev
|
||||
CLAUDE_CONFIG_DIR: /home/agent/.claude
|
||||
POLL_INTERVAL: ${interval}
|
||||
depends_on:
|
||||
- forgejo
|
||||
- woodpecker
|
||||
|
||||
volumes:
|
||||
agent-data-llama:
|
||||
project-repos-llama:
|
||||
OVERRIDEOF
|
||||
|
||||
# Patch the Claude CLI binary path
|
||||
local claude_bin
|
||||
claude_bin="$(command -v claude 2>/dev/null || true)"
|
||||
if [ -n "$claude_bin" ]; then
|
||||
claude_bin="$(readlink -f "$claude_bin")"
|
||||
sed -i "s|CLAUDE_BIN_PLACEHOLDER|${claude_bin}|" "$override_file"
|
||||
else
|
||||
echo " Warning: claude CLI not found — update override file manually"
|
||||
sed -i "s|CLAUDE_BIN_PLACEHOLDER|/usr/local/bin/claude|" "$override_file"
|
||||
# Find project TOML
|
||||
local project_name="${PROJECT_NAME:-}"
|
||||
local toml_file=""
|
||||
if [ -n "$project_name" ]; then
|
||||
toml_file="${FACTORY_ROOT}/projects/${project_name}.toml"
|
||||
fi
|
||||
# Fallback: find the first .toml in projects/
|
||||
if [ -z "$toml_file" ] || [ ! -f "$toml_file" ]; then
|
||||
for f in "${FACTORY_ROOT}/projects/"*.toml; do
|
||||
if [ -f "$f" ]; then
|
||||
toml_file="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo " Created: ${override_file}"
|
||||
if [ -z "$toml_file" ] || [ ! -f "$toml_file" ]; then
|
||||
echo " Error: no project TOML found in ${FACTORY_ROOT}/projects/" >&2
|
||||
echo " Run 'disinto init' first to create a project config" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Project TOML: ${toml_file}"
|
||||
|
||||
# Derive a safe section name from the agent name (lowercase, alphanumeric+hyphens)
|
||||
local section_name
|
||||
section_name=$(echo "$agent_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
|
||||
|
||||
# Default model name if not provided
|
||||
local model="${model_name:-local-model}"
|
||||
|
||||
# Write [agents.<name>] section to the project TOML
|
||||
echo " Writing [agents.${section_name}] to ${toml_file}..."
|
||||
python3 -c '
|
||||
import sys, re, pathlib
|
||||
|
||||
toml_path = sys.argv[1]
|
||||
section_name = sys.argv[2]
|
||||
base_url = sys.argv[3]
|
||||
model = sys.argv[4]
|
||||
agent_name = sys.argv[5]
|
||||
role = sys.argv[6]
|
||||
compact_pct = sys.argv[7]
|
||||
|
||||
p = pathlib.Path(toml_path)
|
||||
text = p.read_text()
|
||||
|
||||
# Build the new section
|
||||
new_section = f"""
|
||||
[agents.{section_name}]
|
||||
base_url = "{base_url}"
|
||||
model = "{model}"
|
||||
api_key = "sk-no-key-required"
|
||||
roles = ["{role}"]
|
||||
forge_user = "{agent_name}"
|
||||
compact_pct = {compact_pct}
|
||||
"""
|
||||
|
||||
# Check if section already exists and replace it
|
||||
pattern = rf"\[agents\.{re.escape(section_name)}\][^\[]*"
|
||||
if re.search(pattern, text):
|
||||
text = re.sub(pattern, new_section.strip() + "\n", text)
|
||||
else:
|
||||
# Remove commented-out example [agents.llama] block if present
|
||||
text = re.sub(
|
||||
r"\n# Local-model agents \(optional\).*?(?=\n# \[mirrors\]|\n\[mirrors\]|\Z)",
|
||||
"",
|
||||
text,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
# Append before [mirrors] if it exists, otherwise at end
|
||||
mirrors_match = re.search(r"\n(# )?\[mirrors\]", text)
|
||||
if mirrors_match:
|
||||
text = text[:mirrors_match.start()] + "\n" + new_section + text[mirrors_match.start():]
|
||||
else:
|
||||
text = text.rstrip() + "\n" + new_section
|
||||
|
||||
p.write_text(text)
|
||||
' "$toml_file" "$section_name" "$local_model" "$model" "$agent_name" "$role" "${poll_interval:-60}"
|
||||
|
||||
echo " Agent config written to TOML"
|
||||
|
||||
# Regenerate docker-compose.yml to include the new agent container
|
||||
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||
if [ -f "$compose_file" ]; then
|
||||
echo " Regenerating docker-compose.yml..."
|
||||
rm -f "$compose_file"
|
||||
# generate_compose is defined in the calling script (bin/disinto) via generators.sh
|
||||
# Use _generate_compose_impl directly since generators.sh is already sourced
|
||||
local forge_port="3000"
|
||||
if [ -n "${FORGE_URL:-}" ]; then
|
||||
forge_port=$(printf '%s' "$FORGE_URL" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
||||
forge_port="${forge_port:-3000}"
|
||||
fi
|
||||
_generate_compose_impl "$forge_port"
|
||||
echo " Compose regenerated with agents-${section_name} service"
|
||||
fi
|
||||
|
||||
local service_name="agents-${section_name}"
|
||||
echo ""
|
||||
echo " Service name: ${service_name}"
|
||||
echo " Poll interval: ${interval}s"
|
||||
echo " Model endpoint: ${local_model}"
|
||||
echo " Model: ${model}"
|
||||
echo ""
|
||||
echo " To start the agent, run:"
|
||||
echo " docker compose --profile local-model up -d ${service_name}"
|
||||
echo " docker compose --profile ${service_name} up -d ${service_name}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue