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>]
|
disinto ci-logs <pipeline> [--step <name>]
|
||||||
Read CI logs from Woodpecker SQLite
|
Read CI logs from Woodpecker SQLite
|
||||||
disinto release <version> Create vault PR for release (e.g., v1.2.0)
|
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)
|
Hire a new agent (create user + .profile repo)
|
||||||
|
|
||||||
Init options:
|
Init options:
|
||||||
|
|
@ -64,6 +64,9 @@ Init options:
|
||||||
|
|
||||||
Hire an agent options:
|
Hire an agent options:
|
||||||
--formula <path> Path to role formula TOML (default: formulas/<role>.toml)
|
--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:
|
CI logs options:
|
||||||
--step <name> Filter logs to a specific step (e.g., smoke-init)
|
--step <name> Filter logs to a specific step (e.g., smoke-init)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# source "${FACTORY_ROOT}/lib/hire-agent.sh"
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -22,11 +22,12 @@ disinto_hire_an_agent() {
|
||||||
local role="${2:-}"
|
local role="${2:-}"
|
||||||
local formula_path=""
|
local formula_path=""
|
||||||
local local_model=""
|
local local_model=""
|
||||||
|
local model_name=""
|
||||||
local poll_interval=""
|
local poll_interval=""
|
||||||
|
|
||||||
if [ -z "$agent_name" ] || [ -z "$role" ]; then
|
if [ -z "$agent_name" ] || [ -z "$role" ]; then
|
||||||
echo "Error: agent-name and role required" >&2
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -42,6 +43,10 @@ disinto_hire_an_agent() {
|
||||||
local_model="$2"
|
local_model="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--model)
|
||||||
|
model_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--poll-interval)
|
--poll-interval)
|
||||||
poll_interval="$2"
|
poll_interval="$2"
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -71,7 +76,8 @@ disinto_hire_an_agent() {
|
||||||
echo "Formula: ${formula_path}"
|
echo "Formula: ${formula_path}"
|
||||||
if [ -n "$local_model" ]; then
|
if [ -n "$local_model" ]; then
|
||||||
echo "Local model: ${local_model}"
|
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
|
fi
|
||||||
|
|
||||||
# Ensure FORGE_TOKEN is set
|
# Ensure FORGE_TOKEN is set
|
||||||
|
|
@ -367,11 +373,6 @@ EOF
|
||||||
echo ""
|
echo ""
|
||||||
echo "Step 6: Configuring local model agent..."
|
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
|
# Validate model endpoint is reachable
|
||||||
echo " Validating model endpoint: ${local_model}"
|
echo " Validating model endpoint: ${local_model}"
|
||||||
if ! curl -sf --max-time 10 "${local_model}/health" >/dev/null 2>&1; then
|
if ! curl -sf --max-time 10 "${local_model}/health" >/dev/null 2>&1; then
|
||||||
|
|
@ -384,83 +385,112 @@ EOF
|
||||||
echo " Model endpoint is reachable"
|
echo " Model endpoint is reachable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate service name from agent name (lowercase)
|
# Find project TOML
|
||||||
local service_name="agents-${agent_name}"
|
local project_name="${PROJECT_NAME:-}"
|
||||||
service_name=$(echo "$service_name" | tr '[:upper:]' '[:lower:]')
|
local toml_file=""
|
||||||
|
if [ -n "$project_name" ]; then
|
||||||
# Set default poll interval
|
toml_file="${FACTORY_ROOT}/projects/${project_name}.toml"
|
||||||
local interval="${poll_interval:-300}"
|
fi
|
||||||
|
# Fallback: find the first .toml in projects/
|
||||||
# Generate the override compose file
|
if [ -z "$toml_file" ] || [ ! -f "$toml_file" ]; then
|
||||||
# Bash expands ${service_name}, ${local_model}, ${interval}, ${PROJECT_NAME} at generation time
|
for f in "${FACTORY_ROOT}/projects/"*.toml; do
|
||||||
# \$HOME, \$FORGE_TOKEN become ${HOME}, ${FORGE_TOKEN} in the file for docker-compose runtime expansion
|
if [ -f "$f" ]; then
|
||||||
cat > "$override_file" <<OVERRIDEOF
|
toml_file="$f"
|
||||||
# docker-compose.override.yml — auto-generated by disinto hire-an-agent
|
break
|
||||||
# Local model agent configuration for ${agent_name}
|
fi
|
||||||
|
done
|
||||||
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"
|
|
||||||
fi
|
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 " Service name: ${service_name}"
|
||||||
echo " Poll interval: ${interval}s"
|
|
||||||
echo " Model endpoint: ${local_model}"
|
echo " Model endpoint: ${local_model}"
|
||||||
|
echo " Model: ${model}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " To start the agent, run:"
|
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
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue