run_multi_agent_turn() (
local main_stream_number=0
local main_stream_ready=false
local main_message_count=0
local max_reconnects="${ARK_MAX_SSE_RECONNECTS:-3}"
local max_history_pages="${ARK_MAX_HISTORY_PAGES:-100}"
local thread_ready_timeout="${ARK_THREAD_READY_TIMEOUT:-30}"
: "${ARK_BASE_URL:?Set ARK_BASE_URL before calling run_multi_agent_turn}"
: "${ARK_API_KEY:?Set ARK_API_KEY before calling run_multi_agent_turn}"
: "${session_id:?Pass a Session ID as the first argument}"
: "${user_text:?Pass a user message as the second argument}"
command -v curl >/dev/null || {
printf 'curl is required.\n' >&2
command -v jq >/dev/null || {
printf 'jq is required.\n' >&2
[[ $max_reconnects =~ ^[0-9]+$ ]] || {
printf 'ARK_MAX_SSE_RECONNECTS must be a non-negative integer.\n' >&2
[[ $max_history_pages =~ ^[1-9][0-9]*$ ]] || {
printf 'ARK_MAX_HISTORY_PAGES must be a positive integer.\n' >&2
[[ $thread_ready_timeout =~ ^[1-9][0-9]*$ ]] || {
printf 'ARK_THREAD_READY_TIMEOUT must be a positive integer.\n' >&2
stream_dir=$(mktemp -d) || {
printf 'Failed to create a temporary directory for SSE streams.\n' >&2
main_error="$stream_dir/main-error.log"
seen_ids_file="$stream_dir/seen-event-ids"
thread_ids_file="$stream_dir/current-thread-ids"
child_pids_file="$stream_dir/child-pids"
terminated_marker="$stream_dir/thread-terminated"
while ! mkdir "$lock_dir" 2>/dev/null; do
rmdir "$1" 2>/dev/null || true
remember_event_if_new() {
[[ -n $event_id ]] || return 0
acquire_lock "$stream_dir/seen.lock"
if grep -Fqx -- "$event_id" "$seen_ids_file"; then
printf '%s\n' "$event_id" >>"$seen_ids_file"
release_lock "$stream_dir/seen.lock"
event_id=$(jq -r '.id // empty' <<<"$event_json" 2>/dev/null || true)
remember_event_if_new "$event_id" || return 0
acquire_lock "$stream_dir/output.lock"
printf '[%s] %s\n' "$source" "$event_json"
release_lock "$stream_dir/output.lock"
acquire_lock "$stream_dir/threads.lock"
if ! grep -Fqx -- "$thread_id" "$thread_ids_file"; then
printf '%s\n' "$thread_id" >>"$thread_ids_file"
release_lock "$stream_dir/threads.lock"
acquire_lock "$stream_dir/pids.lock"
printf '%s\n' "$1" >>"$child_pids_file"
release_lock "$stream_dir/pids.lock"
process_observed_event() {
event_type=$(jq -r '.type // empty' <<<"$event_json" 2>/dev/null || true)
child_thread_id=$(jq -r '.session_thread_id // empty' <<<"$event_json")
if [[ -n $child_thread_id ]]; then
start_thread_monitor "$child_thread_id"
session.thread_status_terminated)
emit_event "$source" "$event_json"
page_count=$((page_count + 1))
if ((page_count > max_history_pages)); then
printf 'Thread %s history exceeded %s pages.\n' \
"$thread_id" "$max_history_pages" >&2
response=$(curl -sS --fail-with-body --get \
"$ARK_BASE_URL/sessions/$session_id/threads/$thread_id/events" \
-H "Authorization: Bearer $ARK_API_KEY" \
--data-urlencode "limit=200" \
--data-urlencode "order=asc" \
--data-urlencode "page=$page") || return 1
response=$(curl -sS --fail-with-body --get \
"$ARK_BASE_URL/sessions/$session_id/threads/$thread_id/events" \
-H "Authorization: Bearer $ARK_API_KEY" \
--data-urlencode "limit=200" \
--data-urlencode "order=asc") || return 1
while IFS= read -r event_json; do
[[ -n $event_json ]] || continue
process_observed_event "$source" "$event_json"
done < <(jq -c '.data[]?' <<<"$response")
page=$(jq -r '.next_page // empty' <<<"$response")
[[ -n $page ]] || return 0
local source="thread:$thread_id"
local stream_error="$stream_dir/thread-$thread_id-error.log"
local stream_ready=false
local thread_reconnect_count=0
if [[ -n $stream_pid ]] && kill -0 "$stream_pid" 2>/dev/null; then
kill "$stream_pid" 2>/dev/null || true
if [[ -n $stream_pid ]]; then
wait "$stream_pid" 2>/dev/null || true
if [[ -n $stream_pipe ]]; then
printf '%s\n' "$1" >"$stream_dir/thread-$thread_id.failed"
trap stop_thread_stream EXIT
stream_number=$((stream_number + 1))
stream_pipe="$stream_dir/thread-$thread_id-$stream_number.pipe"
if ! mkfifo "$stream_pipe"; then
fail_thread_monitor "Failed to create the SSE pipe for thread $thread_id."
curl -sS -N --fail-with-body --max-time 600 \
"$ARK_BASE_URL/sessions/$session_id/threads/$thread_id/stream" \
-H "Authorization: Bearer $ARK_API_KEY" \
-H "Accept: text/event-stream" >"$stream_pipe" 2>>"$stream_error" &
while IFS= read -r thread_line <&3; do
thread_line=${thread_line%$'\r'}
if [[ $thread_line == ": ready" ]]; then
if [[ $stream_ready != true ]]; then
fail_thread_monitor "Thread $thread_id SSE closed before it was ready."
if ! read_thread_history "$thread_id" "$source"; then
fail_thread_monitor "Failed to recover history for thread $thread_id."
: >"$stream_dir/thread-$thread_id.ready"
while IFS= read -r thread_line <&3; do
thread_line=${thread_line%$'\r'}
if [[ $thread_line == ": server draining, reconnect" ]]; then
[[ $thread_line == data:* ]] || continue
thread_data=${thread_line#data:}
thread_data=${thread_data# }
process_observed_event "$source" "$thread_data"
if ((thread_reconnect_count >= max_reconnects)); then
"Thread $thread_id SSE reconnect limit was reached."
thread_reconnect_count=$((thread_reconnect_count + 1))
printf '[%s] reconnecting SSE (%s/%s)\n' \
"$source" "$thread_reconnect_count" "$max_reconnects" >&2
start_thread_monitor() {
if [[ ! $thread_id =~ ^[A-Za-z0-9._-]+$ ]]; then
printf 'Ignoring invalid thread ID: %s\n' "$thread_id" >&2
if mkdir "$stream_dir/thread-$thread_id.started" 2>/dev/null; then
record_thread_id "$thread_id"
monitor_thread "$thread_id" &
record_child_pid "$child_pid"
if [[ -n $main_pid ]] && kill -0 "$main_pid" 2>/dev/null; then
kill "$main_pid" 2>/dev/null || true
if [[ -n $main_pid ]]; then
wait "$main_pid" 2>/dev/null || true
if [[ -n $main_pipe ]]; then
if [[ -f $child_pids_file ]]; then
while IFS= read -r child_pid; do
[[ -n $child_pid ]] || continue
if kill -0 "$child_pid" 2>/dev/null; then
kill "$child_pid" 2>/dev/null || true
done <"$child_pids_file"
while IFS= read -r child_pid; do
[[ -n $child_pid ]] || continue
wait "$child_pid" 2>/dev/null || true
done <"$child_pids_file"
trap cleanup_streams EXIT
main_stream_number=$((main_stream_number + 1))
main_pipe="$stream_dir/main-$main_stream_number.pipe"
if ! mkfifo "$main_pipe"; then
printf 'Failed to create the main SSE pipe.\n' >&2
curl -sS -N --fail-with-body --max-time 600 \
"$ARK_BASE_URL/sessions/$session_id/events/stream" \
-H "Authorization: Bearer $ARK_API_KEY" \
-H "Accept: text/event-stream" >"$main_pipe" 2>>"$main_error" &
while IFS= read -r line <&3; do
if [[ $line == ": ready" ]]; then
if [[ $main_stream_ready != true ]]; then
printf 'Main SSE stream closed before it was ready.\n' >&2
if [[ -s $main_error ]]; then
tail -n 1 "$main_error" >&2
event_type=$(jq -r '.type // empty' <<<"$event_json" 2>/dev/null || true)
main_message_count=$((main_message_count + 1))
retry_status=$(jq -r '.error.retry_status.type // empty' <<<"$event_json")
printf '[main] Session retries were exhausted.\n' >&2
printf '[main] The Session cannot continue after this error.\n' >&2
printf '[main] Unknown error retry status: %s\n' \
stop_reason=$(jq -r '.stop_reason.type // empty' <<<"$event_json")
if ((main_message_count == 0)); then
printf 'The main thread ended without an agent.message event.\n' >&2
'.stop_reason.event_ids // [] | join(",")' <<<"$event_json")
printf '[main] requires_action: %s\n' "$pending_ids" >&2
printf '[main] retries_exhausted\n' >&2
printf 'Unknown session.status_idle stop_reason: %s\n' \
session.status_terminated)
printf '[main] terminated\n' >&2
process_observed_event "main" "$event_json"
page_count=$((page_count + 1))
if ((page_count > max_history_pages)); then
printf 'Main history exceeded %s pages.\n' "$max_history_pages" >&2
response=$(curl -sS --fail-with-body --get \
"$ARK_BASE_URL/sessions/$session_id/events" \
-H "Authorization: Bearer $ARK_API_KEY" \
--data-urlencode "limit=200" \
--data-urlencode "order=asc" \
--data-urlencode "page=$page") || return 1
response=$(curl -sS --fail-with-body --get \
"$ARK_BASE_URL/sessions/$session_id/events" \
-H "Authorization: Bearer $ARK_API_KEY" \
--data-urlencode "limit=200" \
--data-urlencode "order=asc") || return 1
while IFS= read -r event_json; do
[[ -n $event_json ]] || continue
if [[ $mode == "seed" ]]; then
event_id=$(jq -r '.id // empty' <<<"$event_json")
remember_event_if_new "$event_id" || true
process_main_event "$event_json"
[[ $turn_done == true ]] && break
done < <(jq -c '.data[]?' <<<"$response")
[[ $turn_done == true ]] && return 0
page=$(jq -r '.next_page // empty' <<<"$response")
[[ -n $page ]] || return 0
check_thread_monitor_failures() {
for failure_file in "$stream_dir"/thread-*.failed; do
[[ -e $failure_file ]] || continue
wait_for_thread_monitors() {
while ((elapsed < thread_ready_timeout)); do
check_thread_monitor_failures || return 1
while IFS= read -r thread_id; do
[[ -n $thread_id ]] || continue
if [[ ! -f $stream_dir/thread-$thread_id.ready ]]; then
done <"$thread_ids_file"
[[ $all_ready == true ]] && return 0
elapsed=$((elapsed + 1))
printf 'Not all child-thread SSE streams became ready within %s seconds.\n' \
"$thread_ready_timeout" >&2
verify_current_thread_states() {
local listed_ids_file="$stream_dir/listed-current-thread-ids"
page_count=$((page_count + 1))
if ((page_count > max_history_pages)); then
printf 'Thread listing exceeded %s pages.\n' "$max_history_pages" >&2
response=$(curl -sS --fail-with-body --get \
"$ARK_BASE_URL/sessions/$session_id/threads" \
-H "Authorization: Bearer $ARK_API_KEY" \
--data-urlencode "limit=1000" \
--data-urlencode "page=$page") || return 1
response=$(curl -sS --fail-with-body --get \
"$ARK_BASE_URL/sessions/$session_id/threads" \
-H "Authorization: Bearer $ARK_API_KEY" \
--data-urlencode "limit=1000") || return 1
while IFS= read -r thread_json; do
[[ -n $thread_json ]] || continue
thread_id=$(jq -r '.id // empty' <<<"$thread_json")
grep -Fqx -- "$thread_id" "$thread_ids_file" || continue
observed_count=$((observed_count + 1))
printf '%s\n' "$thread_id" >>"$listed_ids_file"
thread_status=$(jq -r '.status // empty' <<<"$thread_json")
printf '[thread-state] %s\n' "$thread_json"
case "$thread_status" in
printf 'Thread %s is still %s.\n' \
"$thread_id" "$thread_status" >&2
printf 'Thread %s was terminated.\n' "$thread_id" >&2
printf 'Thread %s returned unknown status: %s\n' \
"$thread_id" "$thread_status" >&2
done < <(jq -c '.data[]?' <<<"$response")
page=$(jq -r '.next_page // empty' <<<"$response")
if [[ ! -s $thread_ids_file ]]; then
printf 'No child thread was created for this multi-agent turn.\n' >&2
while IFS= read -r thread_id; do
[[ -n $thread_id ]] || continue
if ! grep -Fqx -- "$thread_id" "$listed_ids_file"; then
printf 'Thread %s was observed in events but not in the thread list.\n' \
done <"$thread_ids_file"
if ! start_main_stream; then
if ! read_main_history seed; then
printf 'Failed to read main-thread history before sending the message.\n' >&2
events_payload=$(jq -n --arg text "$user_text" \
'{events: [{type: "user.message", content: [{type: "text", text: $text}]}]}')
curl -sS --fail-with-body \
"$ARK_BASE_URL/sessions/$session_id/events" \
-H "Authorization: Bearer $ARK_API_KEY" \
-H "Content-Type: application/json" \
-d "$events_payload" >/dev/null || return 1
while [[ $turn_done != true ]]; do
while IFS= read -r line <&3; do
if [[ $line == ": server draining, reconnect" ]]; then
[[ $line == data:* ]] || continue
process_main_event "$data"
[[ $turn_done == true ]] && break
[[ $turn_done == true ]] && break
check_thread_monitor_failures || return 1
if ((reconnect_count >= max_reconnects)); then
printf 'Main SSE reconnect limit was reached before the turn finished.\n' >&2
reconnect_count=$((reconnect_count + 1))
printf '[main] reconnecting SSE and recovering history (%s/%s)\n' \
"$reconnect_count" "$max_reconnects" >&2
if ! start_main_stream; then
if ! read_main_history recover; then
printf 'Failed to recover main-thread history after reconnecting.\n' >&2
((result_code == 0)) || return "$result_code"
wait_for_thread_monitors || return 6
check_thread_monitor_failures || return 6
if [[ -f $terminated_marker ]]; then
printf 'A child thread emitted session.thread_status_terminated.\n' >&2
verify_current_thread_states || return 6