agent-control-plane 0.6.0 → 0.7.0

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
+
@@ -734,6 +734,43 @@ refreshButton.addEventListener("click", () => {
734
734
 
735
735
  initializeTheme();
736
736
  void loadSnapshot();
737
- window.setInterval(() => {
738
- void loadSnapshot();
739
- }, 5000);
737
+
738
+ // WebSocket live updates
739
+ let wsReconnectDelay = 1000;
740
+ let wsConnectionActive = false;
741
+
742
+ function connectWebSocket() {
743
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
744
+ const wsUrl = `${protocol}//${location.host}/ws`;
745
+ const ws = new WebSocket(wsUrl);
746
+
747
+ ws.onopen = () => {
748
+ wsReconnectDelay = 1000;
749
+ wsConnectionActive = true;
750
+ console.log("ACP Dashboard: WebSocket connected");
751
+ };
752
+
753
+ ws.onmessage = (event) => {
754
+ try {
755
+ const snapshot = JSON.parse(event.data);
756
+ window._acpSnapshot = snapshot;
757
+ renderFromSnapshot(snapshot);
758
+ maybeNotifyAlerts(snapshot);
759
+ } catch (error) {
760
+ console.error("ACP Dashboard: Failed to parse WebSocket message", error);
761
+ }
762
+ };
763
+
764
+ ws.onclose = () => {
765
+ wsConnectionActive = false;
766
+ console.log(`ACP Dashboard: WebSocket disconnected, reconnecting in ${wsReconnectDelay}ms`);
767
+ setTimeout(connectWebSocket, wsReconnectDelay);
768
+ wsReconnectDelay = Math.min(wsReconnectDelay * 2, 30000);
769
+ };
770
+
771
+ ws.onerror = (error) => {
772
+ console.error("ACP Dashboard: WebSocket error", error);
773
+ };
774
+ }
775
+
776
+ connectWebSocket();
@@ -0,0 +1,3 @@
1
+ aiohttp>=3.8
2
+ aiohttp-cors>=0.7
3
+ websockets>=10.0