agent-control-plane 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,317 @@
1
+ flow_provider_pool_state_get() {
2
+ local config_file="${1:?config file required}"
3
+ local pool_name="${2:?pool name required}"
4
+ local backend=""
5
+ local model=""
6
+ local state_root=""
7
+ local provider_key=""
8
+ local state_file=""
9
+ local attempts="0"
10
+ local next_attempt_epoch="0"
11
+ local next_attempt_at=""
12
+ local last_reason=""
13
+ local updated_at=""
14
+ local ready="yes"
15
+ local valid="yes"
16
+ local now_epoch=""
17
+ local safe_profile=""
18
+ local bypass_profile=""
19
+ local claude_model=""
20
+ local claude_permission_mode=""
21
+ local claude_effort=""
22
+ local claude_timeout_seconds=""
23
+ local claude_max_attempts=""
24
+ local claude_retry_backoff_seconds=""
25
+ local openclaw_model=""
26
+ local openclaw_thinking=""
27
+ local openclaw_timeout_seconds=""
28
+ local ollama_model=""
29
+ local ollama_base_url=""
30
+ local ollama_timeout_seconds=""
31
+ local pi_model=""
32
+ local pi_thinking=""
33
+ local pi_timeout_seconds=""
34
+ local opencode_model=""
35
+ local opencode_timeout_seconds=""
36
+ local kilo_model=""
37
+ local kilo_timeout_seconds=""
38
+
39
+ backend="$(flow_provider_pool_backend "${config_file}" "${pool_name}")"
40
+ safe_profile="$(flow_provider_pool_safe_profile "${config_file}" "${pool_name}")"
41
+ bypass_profile="$(flow_provider_pool_bypass_profile "${config_file}" "${pool_name}")"
42
+ claude_model="$(flow_provider_pool_claude_model "${config_file}" "${pool_name}")"
43
+ claude_permission_mode="$(flow_provider_pool_claude_permission_mode "${config_file}" "${pool_name}")"
44
+ claude_effort="$(flow_provider_pool_claude_effort "${config_file}" "${pool_name}")"
45
+ claude_timeout_seconds="$(flow_provider_pool_claude_timeout_seconds "${config_file}" "${pool_name}")"
46
+ claude_max_attempts="$(flow_provider_pool_claude_max_attempts "${config_file}" "${pool_name}")"
47
+ claude_retry_backoff_seconds="$(flow_provider_pool_claude_retry_backoff_seconds "${config_file}" "${pool_name}")"
48
+ openclaw_model="$(flow_provider_pool_openclaw_model "${config_file}" "${pool_name}")"
49
+ openclaw_thinking="$(flow_provider_pool_openclaw_thinking "${config_file}" "${pool_name}")"
50
+ openclaw_timeout_seconds="$(flow_provider_pool_openclaw_timeout_seconds "${config_file}" "${pool_name}")"
51
+ ollama_model="$(flow_provider_pool_ollama_model "${config_file}" "${pool_name}")"
52
+ ollama_base_url="$(flow_provider_pool_ollama_base_url "${config_file}" "${pool_name}")"
53
+ ollama_timeout_seconds="$(flow_provider_pool_ollama_timeout_seconds "${config_file}" "${pool_name}")"
54
+ pi_model="$(flow_provider_pool_pi_model "${config_file}" "${pool_name}")"
55
+ pi_thinking="$(flow_provider_pool_pi_thinking "${config_file}" "${pool_name}")"
56
+ pi_timeout_seconds="$(flow_provider_pool_pi_timeout_seconds "${config_file}" "${pool_name}")"
57
+ opencode_model="$(flow_provider_pool_opencode_model "${config_file}" "${pool_name}")"
58
+ opencode_timeout_seconds="$(flow_provider_pool_opencode_timeout_seconds "${config_file}" "${pool_name}")"
59
+ kilo_model="$(flow_provider_pool_kilo_model "${config_file}" "${pool_name}")"
60
+ kilo_timeout_seconds="$(flow_provider_pool_kilo_timeout_seconds "${config_file}" "${pool_name}")"
61
+ model="$(flow_provider_pool_model_identity "${config_file}" "${pool_name}")"
62
+
63
+ case "${backend}" in
64
+ codex)
65
+ [[ -n "${safe_profile}" && -n "${bypass_profile}" ]] || valid="no"
66
+ ;;
67
+ claude)
68
+ [[ -n "${claude_model}" && -n "${claude_permission_mode}" && -n "${claude_effort}" && -n "${claude_timeout_seconds}" && -n "${claude_max_attempts}" && -n "${claude_retry_backoff_seconds}" ]] || valid="no"
69
+ ;;
70
+ openclaw)
71
+ [[ -n "${openclaw_model}" && -n "${openclaw_thinking}" && -n "${openclaw_timeout_seconds}" ]] || valid="no"
72
+ ;;
73
+ ollama)
74
+ [[ -n "${ollama_model}" ]] || valid="no"
75
+ ;;
76
+ pi)
77
+ [[ -n "${pi_model}" ]] || valid="no"
78
+ ;;
79
+ opencode)
80
+ [[ -n "${opencode_model}" && -n "${opencode_timeout_seconds}" ]] || valid="no"
81
+ ;;
82
+ kilo)
83
+ [[ -n "${kilo_model}" && -n "${kilo_timeout_seconds}" ]] || valid="no"
84
+ ;;
85
+ *)
86
+ valid="no"
87
+ ;;
88
+ esac
89
+
90
+ if [[ "${valid}" == "yes" && -n "${model}" ]]; then
91
+ state_root="$(flow_resolve_state_root "${config_file}")"
92
+ provider_key="$(flow_sanitize_provider_key "${backend}-${model}")"
93
+ state_file="${state_root}/retries/providers/${provider_key}.env"
94
+
95
+ if [[ -f "${state_file}" ]]; then
96
+ set -a
97
+ # shellcheck source=/dev/null
98
+ source "${state_file}"
99
+ set +a
100
+ attempts="${ATTEMPTS:-0}"
101
+ next_attempt_epoch="${NEXT_ATTEMPT_EPOCH:-0}"
102
+ next_attempt_at="${NEXT_ATTEMPT_AT:-}"
103
+ last_reason="${LAST_REASON:-}"
104
+ updated_at="${UPDATED_AT:-}"
105
+ fi
106
+
107
+ now_epoch="$(date +%s)"
108
+ if [[ "${next_attempt_epoch}" =~ ^[0-9]+$ ]] && (( next_attempt_epoch > now_epoch )); then
109
+ ready="no"
110
+ fi
111
+ else
112
+ ready="no"
113
+ fi
114
+
115
+ printf 'POOL_NAME=%s\n' "${pool_name}"
116
+ printf 'VALID=%s\n' "${valid}"
117
+ printf 'BACKEND=%s\n' "${backend}"
118
+ printf 'MODEL=%s\n' "${model}"
119
+ printf 'PROVIDER_KEY=%s\n' "${provider_key}"
120
+ printf 'ATTEMPTS=%s\n' "${attempts}"
121
+ printf 'NEXT_ATTEMPT_EPOCH=%s\n' "${next_attempt_epoch}"
122
+ printf 'NEXT_ATTEMPT_AT=%s\n' "${next_attempt_at}"
123
+ printf 'READY=%s\n' "${ready}"
124
+ printf 'LAST_REASON=%s\n' "${last_reason}"
125
+ printf 'UPDATED_AT=%s\n' "${updated_at}"
126
+ printf 'SAFE_PROFILE=%s\n' "${safe_profile}"
127
+ printf 'BYPASS_PROFILE=%s\n' "${bypass_profile}"
128
+ printf 'CLAUDE_MODEL=%s\n' "${claude_model}"
129
+ printf 'CLAUDE_PERMISSION_MODE=%s\n' "${claude_permission_mode}"
130
+ printf 'CLAUDE_EFFORT=%s\n' "${claude_effort}"
131
+ printf 'CLAUDE_TIMEOUT_SECONDS=%s\n' "${claude_timeout_seconds}"
132
+ printf 'CLAUDE_MAX_ATTEMPTS=%s\n' "${claude_max_attempts}"
133
+ printf 'CLAUDE_RETRY_BACKOFF_SECONDS=%s\n' "${claude_retry_backoff_seconds}"
134
+ printf 'OPENCLAW_MODEL=%s\n' "${openclaw_model}"
135
+ printf 'OPENCLAW_THINKING=%s\n' "${openclaw_thinking}"
136
+ printf 'OPENCLAW_TIMEOUT_SECONDS=%s\n' "${openclaw_timeout_seconds}"
137
+ printf 'OLLAMA_MODEL=%s\n' "${ollama_model}"
138
+ printf 'OLLAMA_BASE_URL=%s\n' "${ollama_base_url}"
139
+ printf 'OLLAMA_TIMEOUT_SECONDS=%s\n' "${ollama_timeout_seconds}"
140
+ printf 'PI_MODEL=%s\n' "${pi_model}"
141
+ printf 'PI_THINKING=%s\n' "${pi_thinking}"
142
+ printf 'PI_TIMEOUT_SECONDS=%s\n' "${pi_timeout_seconds}"
143
+ printf 'OPENCODE_MODEL=%s\n' "${opencode_model}"
144
+ printf 'OPENCODE_TIMEOUT_SECONDS=%s\n' "${opencode_timeout_seconds}"
145
+ printf 'KILO_MODEL=%s\n' "${kilo_model}"
146
+ printf 'KILO_TIMEOUT_SECONDS=%s\n' "${kilo_timeout_seconds}"
147
+ }
148
+
149
+ flow_selected_provider_pool_env() {
150
+ local config_file="${1:-}"
151
+ local pool_name=""
152
+ local candidate=""
153
+ local candidate_valid=""
154
+ local candidate_ready=""
155
+ local candidate_next_epoch="0"
156
+ local exhausted_candidate=""
157
+ local exhausted_epoch=""
158
+
159
+ if [[ -z "${config_file}" ]]; then
160
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
161
+ fi
162
+
163
+ if ! flow_provider_pools_enabled "${config_file}"; then
164
+ return 1
165
+ fi
166
+
167
+ while IFS= read -r pool_name; do
168
+ [[ -n "${pool_name}" ]] || continue
169
+ candidate="$(flow_provider_pool_state_get "${config_file}" "${pool_name}")"
170
+ candidate_valid="$(awk -F= '/^VALID=/{print $2}' <<<"${candidate}")"
171
+ [[ "${candidate_valid}" == "yes" ]] || continue
172
+
173
+ candidate_ready="$(awk -F= '/^READY=/{print $2}' <<<"${candidate}")"
174
+ if [[ "${candidate_ready}" == "yes" ]]; then
175
+ printf '%s\n' "${candidate}"
176
+ printf 'POOLS_EXHAUSTED=no\n'
177
+ printf 'SELECTION_REASON=ready\n'
178
+ return 0
179
+ fi
180
+
181
+ candidate_next_epoch="$(awk -F= '/^NEXT_ATTEMPT_EPOCH=/{print $2}' <<<"${candidate}")"
182
+ if [[ -z "${exhausted_candidate}" ]]; then
183
+ exhausted_candidate="${candidate}"
184
+ exhausted_epoch="${candidate_next_epoch}"
185
+ continue
186
+ fi
187
+
188
+ if [[ "${candidate_next_epoch}" =~ ^[0-9]+$ && "${exhausted_epoch}" =~ ^[0-9]+$ ]] && (( candidate_next_epoch < exhausted_epoch )); then
189
+ exhausted_candidate="${candidate}"
190
+ exhausted_epoch="${candidate_next_epoch}"
191
+ fi
192
+ done < <(flow_provider_pool_names "${config_file}")
193
+
194
+ [[ -n "${exhausted_candidate}" ]] || return 1
195
+
196
+ printf '%s\n' "${exhausted_candidate}"
197
+ printf 'POOLS_EXHAUSTED=yes\n'
198
+ printf 'SELECTION_REASON=all-cooldown\n'
199
+ }
200
+
201
+ flow_resolve_issue_session_prefix() {
202
+ local config_file="${1:-}"
203
+ local default_value=""
204
+ if [[ -z "${config_file}" ]]; then
205
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
206
+ fi
207
+ default_value="$(flow_default_issue_session_prefix "${config_file}")"
208
+ flow_env_or_config "${config_file}" "ACP_ISSUE_SESSION_PREFIX F_LOSNING_ISSUE_SESSION_PREFIX" "session_naming.issue_prefix" "${default_value}"
209
+ }
210
+
211
+ flow_resolve_pr_session_prefix() {
212
+ local config_file="${1:-}"
213
+ local default_value=""
214
+ if [[ -z "${config_file}" ]]; then
215
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
216
+ fi
217
+ default_value="$(flow_default_pr_session_prefix "${config_file}")"
218
+ flow_env_or_config "${config_file}" "ACP_PR_SESSION_PREFIX F_LOSNING_PR_SESSION_PREFIX" "session_naming.pr_prefix" "${default_value}"
219
+ }
220
+
221
+ flow_resolve_issue_branch_prefix() {
222
+ local config_file="${1:-}"
223
+ local default_value=""
224
+ if [[ -z "${config_file}" ]]; then
225
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
226
+ fi
227
+ default_value="$(flow_default_issue_branch_prefix "${config_file}")"
228
+ flow_env_or_config "${config_file}" "ACP_ISSUE_BRANCH_PREFIX F_LOSNING_ISSUE_BRANCH_PREFIX" "session_naming.issue_branch_prefix" "${default_value}"
229
+ }
230
+
231
+ flow_resolve_pr_worktree_branch_prefix() {
232
+ local config_file="${1:-}"
233
+ local default_value=""
234
+ if [[ -z "${config_file}" ]]; then
235
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
236
+ fi
237
+ default_value="$(flow_default_pr_worktree_branch_prefix "${config_file}")"
238
+ flow_env_or_config "${config_file}" "ACP_PR_WORKTREE_BRANCH_PREFIX F_LOSNING_PR_WORKTREE_BRANCH_PREFIX" "session_naming.pr_worktree_branch_prefix" "${default_value}"
239
+ }
240
+
241
+ flow_resolve_managed_pr_branch_globs() {
242
+ local config_file="${1:-}"
243
+ local default_value=""
244
+ if [[ -z "${config_file}" ]]; then
245
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
246
+ fi
247
+ default_value="$(flow_default_managed_pr_branch_globs "${config_file}")"
248
+ flow_env_or_config "${config_file}" "ACP_MANAGED_PR_BRANCH_GLOBS F_LOSNING_MANAGED_PR_BRANCH_GLOBS" "session_naming.managed_pr_branch_globs" "${default_value}"
249
+ }
250
+
251
+ flow_escape_regex() {
252
+ local raw_value="${1:-}"
253
+ python3 - "${raw_value}" <<'PY'
254
+ import re
255
+ import sys
256
+
257
+ print(re.escape(sys.argv[1]))
258
+ PY
259
+ }
260
+
261
+ flow_managed_pr_prefixes() {
262
+ local config_file="${1:-}"
263
+ local managed_globs=""
264
+ local branch_glob=""
265
+ local prefix=""
266
+
267
+ managed_globs="$(flow_resolve_managed_pr_branch_globs "${config_file}")"
268
+ for branch_glob in ${managed_globs}; do
269
+ prefix="${branch_glob%\*}"
270
+ [[ -n "${prefix}" ]] || continue
271
+ printf '%s\n' "${prefix}"
272
+ done
273
+ }
274
+
275
+ flow_managed_pr_prefixes_json() {
276
+ local config_file="${1:-}"
277
+ local prefixes=()
278
+ local prefix=""
279
+
280
+ while IFS= read -r prefix; do
281
+ [[ -n "${prefix}" ]] || continue
282
+ prefixes+=("${prefix}")
283
+ done < <(flow_managed_pr_prefixes "${config_file}")
284
+
285
+ python3 - "${prefixes[@]}" <<'PY'
286
+ import json
287
+ import sys
288
+
289
+ print(json.dumps(sys.argv[1:]))
290
+ PY
291
+ }
292
+
293
+ flow_managed_issue_branch_regex() {
294
+ local config_file="${1:-}"
295
+ local prefix=""
296
+ local normalized_prefix=""
297
+ local escaped_prefix=""
298
+ local joined=""
299
+
300
+ while IFS= read -r prefix; do
301
+ [[ -n "${prefix}" ]] || continue
302
+ normalized_prefix="${prefix%/}"
303
+ escaped_prefix="$(flow_escape_regex "${normalized_prefix}")"
304
+ if [[ -n "${joined}" ]]; then
305
+ joined="${joined}|${escaped_prefix}"
306
+ else
307
+ joined="${escaped_prefix}"
308
+ fi
309
+ done < <(flow_managed_pr_prefixes "${config_file}")
310
+
311
+ if [[ -z "${joined}" ]]; then
312
+ joined="$(flow_escape_regex "agent/$(flow_resolve_adapter_id "${config_file}")")"
313
+ fi
314
+
315
+ printf '^(?:%s)/issue-(?<id>[0-9]+)(?:-|$)\n' "${joined}"
316
+ }
317
+
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # kilo-adapter.sh
3
+ # Adapter implementation for Kilo Code
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="kilo"
12
+ ADAPTER_NAME="Kilo Code"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${KILO_MODEL:-anthropic/claude-sonnet-4-20250514}"
16
+
17
+ # Kilo capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_MAX_TIMEOUT=900
22
+
23
+ adapter_info() {
24
+ cat <<EOF
25
+ id=${ADAPTER_ID}
26
+ name=${ADAPTER_NAME}
27
+ type=${ADAPTER_TYPE}
28
+ version=${ADAPTER_VERSION}
29
+ model=${ADAPTER_MODEL}
30
+ EOF
31
+ }
32
+
33
+ adapter_health_check() {
34
+ if ! command -v kilo >/dev/null 2>&1; then
35
+ echo "ERROR: kilo CLI not found in PATH"
36
+ return 1
37
+ fi
38
+
39
+ # Verify kilo can actually run (version check)
40
+ if ! kilo --version >/dev/null 2>&1; then
41
+ echo "ERROR: kilo CLI cannot run (check installation)"
42
+ return 1
43
+ fi
44
+
45
+ local version
46
+ version="$(kilo --version 2>/dev/null || true)"
47
+ if [[ -z "$version" ]]; then
48
+ echo "WARN: Could not detect kilo version"
49
+ else
50
+ echo "INFO: Kilo version: $version"
51
+ fi
52
+
53
+ # Verify model is specified
54
+ if [[ -z "${ADAPTER_MODEL}" ]]; then
55
+ echo "WARN: No model specified for Kilo adapter"
56
+ fi
57
+
58
+ echo "OK: Kilo adapter healthy (model: ${ADAPTER_MODEL})"
59
+ return 0
60
+ }
61
+
62
+ adapter_run() {
63
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
64
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
65
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
66
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
67
+
68
+ # Validate prompt file
69
+ if [[ ! -f "${prompt_file}" ]]; then
70
+ echo "ERROR: Prompt file not found: ${prompt_file}"
71
+ return 1
72
+ fi
73
+ if [[ ! -s "${prompt_file}" ]]; then
74
+ echo "ERROR: Prompt file is empty: ${prompt_file}"
75
+ return 1
76
+ fi
77
+
78
+ local timeout_seconds="${KILO_TIMEOUT_SECONDS:-900}"
79
+
80
+ echo "Kilo adapter: Running session ${session} with model ${ADAPTER_MODEL}"
81
+
82
+ cd "${worktree}" || return 1
83
+
84
+ prompt="$(cat "${prompt_file}")"
85
+
86
+ # Run kilo and capture output
87
+ local output
88
+ if ! output="$(timeout "${timeout_seconds}" kilo --model "${ADAPTER_MODEL}" "${prompt}" 2>&1)"; then
89
+ echo "ERROR: Kilo run failed or timed out after ${timeout_seconds}s"
90
+ return 1
91
+ fi
92
+
93
+ # Validate JSON stream output (kilo outputs JSON events)
94
+ if ! echo "$output" | python3 -c "import sys, json; [json.loads(line) for line in sys.stdin if line.strip()]" 2>/dev/null; then
95
+ echo "WARN: Kilo output is not valid JSON stream"
96
+ else
97
+ echo "INFO: Kilo output validated as JSON stream"
98
+ fi
99
+
100
+ echo "Kilo adapter: Session ${session} completed"
101
+ return 0
102
+ }
103
+
104
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
105
+ adapter_info
106
+ echo "---"
107
+ adapter_health_check
108
+ fi
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # ollama-adapter.sh
3
+ # Adapter implementation for Ollama local models
4
+ # Implements: adapter-interface.sh
5
+
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "${SCRIPT_DIR}/adapter-interface.sh"
10
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
11
+
12
+ # Ollama adapter metadata
13
+ ADAPTER_ID="ollama"
14
+ ADAPTER_NAME="Ollama Local Models"
15
+ ADAPTER_TYPE="local-model"
16
+ ADAPTER_VERSION="1.0.0"
17
+ ADAPTER_MODEL="${OLLAMA_MODEL:-qwen2.5-coder:7b}"
18
+ ADAPTER_BASE_URL="${OLLAMA_BASE_URL:-http://localhost:11434}"
19
+
20
+ # Ollama capabilities
21
+ ADAPTER_CAP_LOCAL_MODEL=true
22
+ ADAPTER_CAP_STREAMING=true
23
+ ADAPTER_CAP_TOOLS_SUPPORT=true
24
+ ADAPTER_CAP_CONTEXT_WINDOW=32768 # Default, will be detected dynamically
25
+ ADAPTER_CAP_MAX_TIMEOUT=3600
26
+
27
+ # Print adapter info
28
+ adapter_info() {
29
+ cat <<EOF
30
+ id=${ADAPTER_ID}
31
+ name=${ADAPTER_NAME}
32
+ type=${ADAPTER_TYPE}
33
+ version=${ADAPTER_VERSION}
34
+ model=${ADAPTER_MODEL}
35
+ base_url=${ADAPTER_BASE_URL}
36
+ EOF
37
+ }
38
+
39
+ # Health check: verify ollama is running and model is available
40
+ adapter_health_check() {
41
+ # Check if ollama is running
42
+ if ! curl -sf "${ADAPTER_BASE_URL}/api/tags" >/dev/null 2>&1; then
43
+ echo "ERROR: Ollama not reachable at ${ADAPTER_BASE_URL}"
44
+ return 1
45
+ fi
46
+
47
+ # Check if model is available (try to pull if not)
48
+ if ! ollama list 2>/dev/null | grep -q "${ADAPTER_MODEL}"; then
49
+ echo "WARN: Model ${ADAPTER_MODEL} not found locally. Attempting pull..."
50
+ if ! ollama pull "${ADAPTER_MODEL}" 2>&1; then
51
+ echo "ERROR: Failed to pull model ${ADAPTER_MODEL}"
52
+ return 1
53
+ fi
54
+ fi
55
+
56
+ # Detect context window and update capability dynamically
57
+ local context_window
58
+ # Ollama API returns context_length inside model_info with architecture prefix
59
+ # e.g., qwen2.context_length, llama.context_length, etc.
60
+ if command -v jq &>/dev/null; then
61
+ context_window="$(curl -sf "${ADAPTER_BASE_URL}/api/show" -d "{\"name\":\"${ADAPTER_MODEL}\"}" 2>/dev/null | jq -r '.model_info // {} | to_entries[] | select(.key | endswith("context_length")) | .value' 2>/dev/null | head -1 || true)"
62
+ else
63
+ # Fallback: use grep for common patterns
64
+ context_window="$(curl -sf "${ADAPTER_BASE_URL}/api/show" -d "{\"name\":\"${ADAPTER_MODEL}\"}" 2>/dev/null | grep -o '"[a-z]*\.context_length":[0-9]*' | head -1 | grep -o '[0-9]*$' || true)"
65
+ fi
66
+ if [[ -n "$context_window" ]]; then
67
+ ADAPTER_CAP_CONTEXT_WINDOW="$context_window"
68
+ echo "INFO: Detected context window: $context_window tokens"
69
+ else
70
+ echo "WARN: Could not detect context window from Ollama API"
71
+ fi
72
+
73
+ echo "OK: Ollama healthy, model ${ADAPTER_MODEL} available"
74
+ return 0
75
+ }
76
+
77
+ # Run a task using ollama
78
+ adapter_run() {
79
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
80
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
81
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
82
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
83
+
84
+ # Validate prompt file
85
+ if [[ ! -f "${prompt_file}" ]]; then
86
+ echo "ERROR: Prompt file not found: ${prompt_file}"
87
+ return 1
88
+ fi
89
+ if [[ ! -s "${prompt_file}" ]]; then
90
+ echo "ERROR: Prompt file is empty: ${prompt_file}"
91
+ return 1
92
+ fi
93
+
94
+ local timeout_seconds="${OLLAMA_TIMEOUT_SECONDS:-900}"
95
+
96
+ echo "Ollama adapter: Running session ${session} with model ${ADAPTER_MODEL}"
97
+
98
+ # Read the prompt
99
+ local prompt
100
+ prompt="$(cat "${prompt_file}")"
101
+
102
+ # Run ollama with the prompt
103
+ # Use perl for timeout on macOS (which lacks GNU timeout)
104
+ if command -v timeout >/dev/null 2>&1; then
105
+ if ! timeout "${timeout_seconds}" ollama run "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
106
+ echo "ERROR: Ollama run failed or timed out after ${timeout_seconds}s"
107
+ return 1
108
+ fi
109
+ elif command -v perl >/dev/null 2>&1; then
110
+ if ! perl -e "alarm ${timeout_seconds}; exec @ARGV" ollama run "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
111
+ echo "ERROR: Ollama run failed or timed out after ${timeout_seconds}s"
112
+ return 1
113
+ fi
114
+ else
115
+ # No timeout available, run without timeout
116
+ if ! ollama run "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
117
+ echo "ERROR: Ollama run failed"
118
+ return 1
119
+ fi
120
+ fi
121
+
122
+ echo "Ollama adapter: Session ${session} completed"
123
+ return 0
124
+ }
125
+
126
+ # Status check (override default)
127
+ adapter_status() {
128
+ local runs_root="${1:?usage: adapter_status RUNS_ROOT SESSION}"
129
+ local session="${2:?usage: adapter_status RUNS_ROOT SESSION}"
130
+ local run_dir="${runs_root}/${session}"
131
+
132
+ if [[ ! -d "$run_dir" ]]; then
133
+ echo "NOT_FOUND"
134
+ return 1
135
+ fi
136
+
137
+ # Check for result file
138
+ if [[ -f "$run_dir/result.env" ]]; then
139
+ source "$run_dir/result.env"
140
+ echo "${OUTCOME:-UNKNOWN}"
141
+ return 0
142
+ fi
143
+
144
+ # Check if ollama process is running
145
+ if pgrep -f "ollama run ${ADAPTER_MODEL}" >/dev/null 2>&1; then
146
+ echo "RUNNING"
147
+ return 0
148
+ fi
149
+
150
+ echo "UNKNOWN"
151
+ return 0
152
+ }
153
+
154
+ # Self-register: validate this adapter implements required functions
155
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
156
+ # Running directly - print info
157
+ adapter_info
158
+ echo "---"
159
+ adapter_health_check
160
+ fi
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # openclaw-adapter.sh
3
+ # Adapter implementation for OpenClaw.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR}/adapter-interface.sh"
9
+ source "${SCRIPT_DIR}/adapter-capabilities.sh"
10
+
11
+ ADAPTER_ID="openclaw"
12
+ ADAPTER_NAME="OpenClaw"
13
+ ADAPTER_TYPE="cloud-api"
14
+ ADAPTER_VERSION="1.0.0"
15
+ ADAPTER_MODEL="${OPENCLAW_MODEL:-openrouter/qwen/qwen3.5-plus:free}"
16
+
17
+ # OpenClaw capabilities
18
+ ADAPTER_CAP_CLOUD_API=true
19
+ ADAPTER_CAP_STREAMING=true
20
+ ADAPTER_CAP_JSON_OUTPUT=true
21
+ ADAPTER_CAP_RESIDENT_MODE=true
22
+ ADAPTER_CAP_MAX_TIMEOUT=900
23
+
24
+ adapter_info() {
25
+ cat <<EOF
26
+ id=${ADAPTER_ID}
27
+ name=${ADAPTER_NAME}
28
+ type=${ADAPTER_TYPE}
29
+ version=${ADAPTER_VERSION}
30
+ model=${ADAPTER_MODEL}
31
+ EOF
32
+ }
33
+
34
+ adapter_health_check() {
35
+ if ! command -v openclaw >/dev/null 2>&1; then
36
+ echo "ERROR: openclaw CLI not found in PATH"
37
+ return 1
38
+ fi
39
+ echo "OK: OpenClaw adapter healthy"
40
+ return 0
41
+ }
42
+
43
+ adapter_run() {
44
+ local mode="${1:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
45
+ local session="${2:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
46
+ local worktree="${3:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
47
+ local prompt_file="${4:?usage: adapter_run MODE SESSION WORKTREE PROMPT_FILE}"
48
+
49
+ local timeout_seconds="${OPENCLAW_TIMEOUT_SECONDS:-900}"
50
+
51
+ echo "OpenClaw adapter: Running session ${session}"
52
+
53
+ cd "${worktree}" || return 1
54
+
55
+ prompt="$(cat "${prompt_file}")"
56
+
57
+ if ! timeout "${timeout_seconds}" openclaw --model "${ADAPTER_MODEL}" "${prompt}" 2>&1; then
58
+ echo "ERROR: OpenClaw run failed"
59
+ return 1
60
+ fi
61
+
62
+ return 0
63
+ }
64
+
65
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
66
+ adapter_info
67
+ echo "---"
68
+ adapter_health_check
69
+ fi