@zachjxyz/moxie 0.5.0 → 0.6.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.
- package/bin/moxie +1 -1
- package/lib/agents.sh +70 -201
- package/lib/gateway-agent.mjs +58 -6
- package/lib/phases.sh +111 -348
- package/lib/select-agents.mjs +417 -0
- package/lib/select-gateway.mjs +243 -0
- package/package.json +1 -1
package/bin/moxie
CHANGED
package/lib/agents.sh
CHANGED
|
@@ -368,21 +368,23 @@ print('1' if d.get('$name', False) else '0')
|
|
|
368
368
|
# Persist degradation state to disk (called after degrading an agent)
|
|
369
369
|
_save_degraded() {
|
|
370
370
|
local deg_file="$MOXIE_DIR/degraded.json"
|
|
371
|
+
local deg_tmp="${deg_file}.tmp"
|
|
371
372
|
python3 -c "
|
|
372
|
-
import json
|
|
373
|
+
import json, sys
|
|
373
374
|
existing = {}
|
|
374
375
|
try:
|
|
375
376
|
with open('$deg_file') as f:
|
|
376
377
|
existing = json.load(f)
|
|
377
378
|
except: pass
|
|
378
379
|
names = '${AGENT_NAMES[*]}'.split()
|
|
380
|
+
degraded_flags = '${AGENT_DEGRADED[*]}'.split()
|
|
379
381
|
for i, name in enumerate(names):
|
|
380
|
-
degraded_flags = '${AGENT_DEGRADED[*]}'.split()
|
|
381
382
|
if i < len(degraded_flags) and degraded_flags[i] == '1':
|
|
382
383
|
existing[name] = True
|
|
383
|
-
with open('$
|
|
384
|
+
with open('$deg_tmp', 'w') as f:
|
|
384
385
|
json.dump(existing, f, indent=2)
|
|
385
|
-
" 2
|
|
386
|
+
" || { echo "WARNING: Failed to save degradation state" >&2; return; }
|
|
387
|
+
mv "$deg_tmp" "$deg_file"
|
|
386
388
|
}
|
|
387
389
|
|
|
388
390
|
# Returns 0 if the agent is degraded, 1 otherwise.
|
|
@@ -398,10 +400,11 @@ is_agent_degraded() {
|
|
|
398
400
|
}
|
|
399
401
|
|
|
400
402
|
# Record a productive or non-productive turn for an agent.
|
|
401
|
-
# Usage: _record_turn_health <agent_name> <logfile>
|
|
403
|
+
# Usage: _record_turn_health <agent_name> <logfile> [exit_code]
|
|
402
404
|
_record_turn_health() {
|
|
403
405
|
local name="$1"
|
|
404
406
|
local logfile="$2"
|
|
407
|
+
local exit_code="${3:-0}"
|
|
405
408
|
|
|
406
409
|
local idx=-1
|
|
407
410
|
for i in "${!AGENT_NAMES[@]}"; do
|
|
@@ -419,9 +422,13 @@ _record_turn_health() {
|
|
|
419
422
|
log_size=$(wc -c < "$logfile" | tr -d ' ')
|
|
420
423
|
fi
|
|
421
424
|
|
|
422
|
-
# Check for known failure signatures
|
|
425
|
+
# Check for known failure signatures
|
|
423
426
|
local is_failure=0
|
|
424
|
-
if [ "$
|
|
427
|
+
if [ "$exit_code" = "124" ]; then
|
|
428
|
+
is_failure=1 # timeout
|
|
429
|
+
elif [ "$exit_code" != "0" ] && [ "$log_size" -lt 1024 ]; then
|
|
430
|
+
is_failure=1 # non-zero exit with minimal output
|
|
431
|
+
elif [ "$log_size" -lt 1024 ]; then
|
|
425
432
|
is_failure=1
|
|
426
433
|
elif grep -qE '^\[TIMEOUT\]' "$logfile" 2>/dev/null; then
|
|
427
434
|
is_failure=1
|
|
@@ -526,15 +533,20 @@ dispatch_logged() {
|
|
|
526
533
|
return 0
|
|
527
534
|
fi
|
|
528
535
|
|
|
529
|
-
|
|
536
|
+
# Capture exit code through the pipe (tee masks it)
|
|
537
|
+
local rc_file
|
|
538
|
+
rc_file=$(mktemp "${TMPDIR:-/tmp}/moxie-rc.XXXXXX")
|
|
539
|
+
( dispatch_agent "$agent" "$prompt_file" "$timeout_secs" "$phase" 2>&1; echo $? > "$rc_file" ) | tee "$logfile"
|
|
540
|
+
local dispatch_rc=0
|
|
541
|
+
[ -f "$rc_file" ] && dispatch_rc=$(cat "$rc_file") && rm -f "$rc_file"
|
|
530
542
|
|
|
531
543
|
local tokens
|
|
532
544
|
tokens=$(extract_tokens "$logfile" "$agent")
|
|
533
545
|
record_tokens "$csv_file" "$turn" "$agent" "$ts" "$tokens" "$phase"
|
|
534
546
|
echo " Tokens: $tokens"
|
|
535
547
|
|
|
536
|
-
# Track agent health based on output
|
|
537
|
-
_record_turn_health "$agent" "$logfile" || return $?
|
|
548
|
+
# Track agent health based on output and exit code
|
|
549
|
+
_record_turn_health "$agent" "$logfile" "$dispatch_rc" || return $?
|
|
538
550
|
}
|
|
539
551
|
|
|
540
552
|
# ---- Rewrite agents section of config.toml ----
|
|
@@ -612,8 +624,11 @@ if '[settings]' in cleaned:
|
|
|
612
624
|
else:
|
|
613
625
|
cleaned = cleaned.rstrip() + '\n\n' + agents_block
|
|
614
626
|
|
|
615
|
-
|
|
627
|
+
tmp_path = config_path + '.tmp'
|
|
628
|
+
with open(tmp_path, 'w') as f:
|
|
616
629
|
f.write(cleaned)
|
|
630
|
+
import os
|
|
631
|
+
os.rename(tmp_path, config_path)
|
|
617
632
|
" "$MOXIE_CONFIG" \
|
|
618
633
|
"$(IFS='|'; echo "${cli_agents[*]}")" \
|
|
619
634
|
"$(IFS='|'; echo "${cli_cmds[*]}")" \
|
|
@@ -733,205 +748,59 @@ _cmd_agents_swap() {
|
|
|
733
748
|
[ -n "$cfg_ep" ] && endpoint="$cfg_ep"
|
|
734
749
|
fi
|
|
735
750
|
|
|
736
|
-
#
|
|
737
|
-
|
|
738
|
-
local
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
751
|
+
# Build current models JSON
|
|
752
|
+
local current_json="["
|
|
753
|
+
local first=1
|
|
754
|
+
if [ ${#current_gw_models[@]} -gt 0 ]; then
|
|
755
|
+
for cm in "${current_gw_models[@]}"; do
|
|
756
|
+
[ "$first" = "0" ] && current_json="${current_json},"
|
|
757
|
+
first=0
|
|
758
|
+
current_json="${current_json}\"$cm\""
|
|
759
|
+
done
|
|
745
760
|
fi
|
|
761
|
+
current_json="${current_json}]"
|
|
762
|
+
|
|
763
|
+
# Run the Node.js swap picker
|
|
764
|
+
local swap_output=""
|
|
765
|
+
swap_output=$(
|
|
766
|
+
GATEWAY_API_KEY="$api_key" \
|
|
767
|
+
GATEWAY_ENDPOINT="$endpoint" \
|
|
768
|
+
MOXIE_CURRENT_MODELS="$current_json" \
|
|
769
|
+
node "$MOXIE_LIB/select-gateway.mjs" < /dev/tty 2>/dev/tty
|
|
770
|
+
) || {
|
|
771
|
+
echo "ERROR: Model selection failed." >&2
|
|
772
|
+
return 1
|
|
773
|
+
}
|
|
746
774
|
|
|
747
|
-
# Parse
|
|
748
|
-
local
|
|
749
|
-
local
|
|
775
|
+
# Parse result
|
|
776
|
+
local cancelled=""
|
|
777
|
+
local new_gw_names=()
|
|
778
|
+
local new_gw_models=()
|
|
750
779
|
eval "$(python3 -c "
|
|
751
780
|
import json, sys, shlex
|
|
752
781
|
data = json.loads(sys.stdin.read())
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
print('
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
_matched=1
|
|
772
|
-
break
|
|
773
|
-
fi
|
|
774
|
-
done
|
|
775
|
-
sselected+=("$_matched")
|
|
776
|
-
done
|
|
777
|
-
|
|
778
|
-
# Interactive search TUI (same as init picker)
|
|
779
|
-
local search=""
|
|
780
|
-
local scursor=0
|
|
781
|
-
local max_visible=15
|
|
782
|
-
|
|
783
|
-
_swap_render() {
|
|
784
|
-
local filtered=()
|
|
785
|
-
local lc_search
|
|
786
|
-
lc_search=$(echo "$search" | tr '[:upper:]' '[:lower:]')
|
|
787
|
-
for (( i = 0; i < ${#all_model_ids[@]}; i++ )); do
|
|
788
|
-
if [ -z "$search" ]; then
|
|
789
|
-
filtered+=("$i")
|
|
790
|
-
else
|
|
791
|
-
local lc_id
|
|
792
|
-
lc_id=$(echo "${all_model_ids[$i]}" | tr '[:upper:]' '[:lower:]')
|
|
793
|
-
[[ "$lc_id" == *"$lc_search"* ]] && filtered+=("$i")
|
|
794
|
-
fi
|
|
795
|
-
done
|
|
796
|
-
|
|
797
|
-
local fcount=${#filtered[@]}
|
|
798
|
-
[ "$scursor" -ge "$fcount" ] && scursor=$(( fcount > 0 ? fcount - 1 : 0 ))
|
|
799
|
-
[ "$scursor" -lt 0 ] && scursor=0
|
|
800
|
-
|
|
801
|
-
local vstart=0
|
|
802
|
-
if [ "$scursor" -ge "$max_visible" ]; then
|
|
803
|
-
vstart=$(( scursor - max_visible + 1 ))
|
|
804
|
-
fi
|
|
805
|
-
local vend=$(( vstart + max_visible ))
|
|
806
|
-
[ "$vend" -gt "$fcount" ] && vend=$fcount
|
|
807
|
-
|
|
808
|
-
local sel_count=0
|
|
809
|
-
for (( i = 0; i < ${#sselected[@]}; i++ )); do
|
|
810
|
-
[ "${sselected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
811
|
-
done
|
|
812
|
-
|
|
813
|
-
printf "\\r\\033[K \\033[1mSwap gateway models\\033[0m (%d available, %d selected)\\n" "$fcount" "$sel_count" >&2
|
|
814
|
-
printf "\\r\\033[K Search: \\033[36m%s\\033[0m\\033[2m|\\033[0m\\n\\n" "$search" >&2
|
|
815
|
-
|
|
816
|
-
local rendered=0
|
|
817
|
-
for (( vi = vstart; vi < vend; vi++ )); do
|
|
818
|
-
local ri=${filtered[$vi]}
|
|
819
|
-
local mid="${all_model_ids[$ri]}"
|
|
820
|
-
local marker=" "
|
|
821
|
-
[ "$vi" -eq "$scursor" ] && marker="> "
|
|
822
|
-
local check="[ ]"
|
|
823
|
-
[ "${sselected[$ri]}" = "1" ] && check="[x]"
|
|
824
|
-
printf "\\r\\033[K %s%s %s\\n" "$marker" "$check" "$mid" >&2
|
|
825
|
-
rendered=$(( rendered + 1 ))
|
|
826
|
-
done
|
|
827
|
-
|
|
828
|
-
while [ "$rendered" -lt "$max_visible" ]; do
|
|
829
|
-
printf "\\r\\033[K\\n" >&2
|
|
830
|
-
rendered=$(( rendered + 1 ))
|
|
831
|
-
done
|
|
832
|
-
|
|
833
|
-
if [ "$vstart" -gt 0 ] || [ "$vend" -lt "$fcount" ]; then
|
|
834
|
-
printf "\\r\\033[K \\033[2m(%d-%d of %d · scroll for more)\\033[0m\\n" "$(( vstart + 1 ))" "$vend" "$fcount" >&2
|
|
835
|
-
else
|
|
836
|
-
printf "\\r\\033[K\\n" >&2
|
|
837
|
-
fi
|
|
838
|
-
|
|
839
|
-
printf "\\r\\033[K \\033[2m↑↓ navigate · space select · enter confirm · esc cancel · type to filter\\033[0m" >&2
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
local swap_lines=$(( max_visible + 5 ))
|
|
843
|
-
|
|
844
|
-
printf "\\033[?25l" >&2
|
|
845
|
-
local old_stty
|
|
846
|
-
old_stty=$(stty -g < /dev/tty 2>/dev/null)
|
|
847
|
-
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
848
|
-
|
|
849
|
-
_swap_cleanup() {
|
|
850
|
-
printf "\\033[?25h" >&2
|
|
851
|
-
stty "$old_stty" < /dev/tty 2>/dev/null
|
|
852
|
-
}
|
|
853
|
-
trap '_swap_cleanup' INT TERM
|
|
854
|
-
|
|
855
|
-
_swap_render
|
|
856
|
-
|
|
857
|
-
local swap_done=0
|
|
858
|
-
local swap_cancelled=0
|
|
859
|
-
while [ "$swap_done" = "0" ]; do
|
|
860
|
-
local skey
|
|
861
|
-
skey=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || break
|
|
862
|
-
|
|
863
|
-
if [ "$skey" = $'\033' ]; then
|
|
864
|
-
local sbracket sdir
|
|
865
|
-
sbracket=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
866
|
-
sdir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
867
|
-
if [ "$sbracket" = "[" ]; then
|
|
868
|
-
case "$sdir" in
|
|
869
|
-
A) scursor=$(( scursor - 1 )); [ "$scursor" -lt 0 ] && scursor=0 ;;
|
|
870
|
-
B) scursor=$(( scursor + 1 )) ;;
|
|
871
|
-
esac
|
|
872
|
-
else
|
|
873
|
-
swap_cancelled=1
|
|
874
|
-
swap_done=1
|
|
875
|
-
fi
|
|
876
|
-
elif [ "$skey" = " " ]; then
|
|
877
|
-
local ft=()
|
|
878
|
-
local lcs
|
|
879
|
-
lcs=$(echo "$search" | tr '[:upper:]' '[:lower:]')
|
|
880
|
-
for (( i = 0; i < ${#all_model_ids[@]}; i++ )); do
|
|
881
|
-
if [ -z "$search" ]; then
|
|
882
|
-
ft+=("$i")
|
|
883
|
-
else
|
|
884
|
-
local lci
|
|
885
|
-
lci=$(echo "${all_model_ids[$i]}" | tr '[:upper:]' '[:lower:]')
|
|
886
|
-
[[ "$lci" == *"$lcs"* ]] && ft+=("$i")
|
|
887
|
-
fi
|
|
888
|
-
done
|
|
889
|
-
if [ "$scursor" -lt "${#ft[@]}" ]; then
|
|
890
|
-
local tidx=${ft[$scursor]}
|
|
891
|
-
[ "${sselected[$tidx]}" = "0" ] && sselected[$tidx]=1 || sselected[$tidx]=0
|
|
892
|
-
fi
|
|
893
|
-
elif [ "$skey" = "" ]; then
|
|
894
|
-
swap_done=1
|
|
895
|
-
elif [ "$skey" = $'\177' ] || [ "$skey" = $'\010' ]; then
|
|
896
|
-
if [ -n "$search" ]; then
|
|
897
|
-
search="${search%?}"
|
|
898
|
-
scursor=0
|
|
899
|
-
fi
|
|
900
|
-
else
|
|
901
|
-
if [[ "$skey" =~ [[:print:]] ]]; then
|
|
902
|
-
search="${search}${skey}"
|
|
903
|
-
scursor=0
|
|
904
|
-
fi
|
|
905
|
-
fi
|
|
906
|
-
|
|
907
|
-
printf "\\033[%dA" "$swap_lines" >&2
|
|
908
|
-
_swap_render
|
|
909
|
-
done
|
|
910
|
-
|
|
911
|
-
_swap_cleanup
|
|
912
|
-
trap - INT TERM
|
|
913
|
-
printf "\\n\\n" >&2
|
|
914
|
-
|
|
915
|
-
if [ "$swap_cancelled" = "1" ]; then
|
|
782
|
+
if data.get('cancelled', False):
|
|
783
|
+
print('cancelled=1')
|
|
784
|
+
else:
|
|
785
|
+
print('cancelled=0')
|
|
786
|
+
names = []
|
|
787
|
+
models = []
|
|
788
|
+
for mid in data.get('gateway_models', []):
|
|
789
|
+
prov = mid.split('/')[0] if '/' in mid else 'unknown'
|
|
790
|
+
mname = mid.split('/', 1)[1] if '/' in mid else mid
|
|
791
|
+
slug = (prov + '-' + mname).lower().replace(' ', '-').replace('.', '-')
|
|
792
|
+
slug = ''.join(c for c in slug if c.isalnum() or c == '-') + '-gw'
|
|
793
|
+
names.append(slug)
|
|
794
|
+
models.append(mid)
|
|
795
|
+
print('new_gw_names=(' + ' '.join(shlex.quote(n) for n in names) + ')')
|
|
796
|
+
print('new_gw_models=(' + ' '.join(shlex.quote(m) for m in models) + ')')
|
|
797
|
+
" <<< "$swap_output")"
|
|
798
|
+
|
|
799
|
+
if [ "$cancelled" = "1" ]; then
|
|
916
800
|
echo "Cancelled — no changes made."
|
|
917
801
|
return 0
|
|
918
802
|
fi
|
|
919
803
|
|
|
920
|
-
# Collect selected models
|
|
921
|
-
local new_gw_names=()
|
|
922
|
-
local new_gw_models=()
|
|
923
|
-
for (( i = 0; i < ${#sselected[@]}; i++ )); do
|
|
924
|
-
[ "${sselected[$i]}" != "1" ] && continue
|
|
925
|
-
local mid="${all_model_ids[$i]}"
|
|
926
|
-
local prov="${mid%%/*}"
|
|
927
|
-
local mname="${mid#*/}"
|
|
928
|
-
local slug
|
|
929
|
-
slug=$(echo "${prov}-${mname}" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
|
|
930
|
-
slug="${slug}-gw"
|
|
931
|
-
new_gw_names+=("$slug")
|
|
932
|
-
new_gw_models+=("$mid")
|
|
933
|
-
done
|
|
934
|
-
|
|
935
804
|
local total_agents=$(( ${#cli_agents[@]} + ${#new_gw_names[@]} ))
|
|
936
805
|
if [ "$total_agents" -lt 2 ]; then
|
|
937
806
|
echo "ERROR: Need at least 2 agents total (${#cli_agents[@]} CLI + ${#new_gw_names[@]} gateway = $total_agents)." >&2
|
package/lib/gateway-agent.mjs
CHANGED
|
@@ -27,10 +27,21 @@ if (!MODEL) { process.stderr.write('ERROR: GATEWAY_MODEL not set\n'); process.ex
|
|
|
27
27
|
const prompt = process.argv[2];
|
|
28
28
|
if (!prompt) { process.stderr.write('ERROR: No prompt provided\n'); process.exit(1); }
|
|
29
29
|
|
|
30
|
+
// ---- Path safety ----
|
|
31
|
+
|
|
32
|
+
function safePath(p) {
|
|
33
|
+
const resolved = resolve(CWD, p);
|
|
34
|
+
if (resolved !== CWD && !resolved.startsWith(CWD + '/')) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
// ---- Tool implementations ----
|
|
31
41
|
|
|
32
42
|
function toolReadFile(args) {
|
|
33
|
-
const filepath =
|
|
43
|
+
const filepath = safePath(args.path);
|
|
44
|
+
if (!filepath) return `Error: Path escapes project root: ${args.path}`;
|
|
34
45
|
if (!existsSync(filepath)) return `Error: File not found: ${args.path}`;
|
|
35
46
|
const content = readFileSync(filepath, 'utf8');
|
|
36
47
|
const lines = content.split('\n');
|
|
@@ -41,14 +52,16 @@ function toolReadFile(args) {
|
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
function toolWriteFile(args) {
|
|
44
|
-
const filepath =
|
|
55
|
+
const filepath = safePath(args.path);
|
|
56
|
+
if (!filepath) return `Error: Path escapes project root: ${args.path}`;
|
|
45
57
|
mkdirSync(dirname(filepath), { recursive: true });
|
|
46
58
|
writeFileSync(filepath, args.content, 'utf8');
|
|
47
59
|
return `Written ${args.content.length} bytes to ${args.path}`;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
function toolAppendFile(args) {
|
|
51
|
-
const filepath =
|
|
63
|
+
const filepath = safePath(args.path);
|
|
64
|
+
if (!filepath) return `Error: Path escapes project root: ${args.path}`;
|
|
52
65
|
mkdirSync(dirname(filepath), { recursive: true });
|
|
53
66
|
const existing = existsSync(filepath) ? readFileSync(filepath, 'utf8') : '';
|
|
54
67
|
writeFileSync(filepath, existing + args.content, 'utf8');
|
|
@@ -56,7 +69,8 @@ function toolAppendFile(args) {
|
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
function toolEditFile(args) {
|
|
59
|
-
const filepath =
|
|
72
|
+
const filepath = safePath(args.path);
|
|
73
|
+
if (!filepath) return `Error: Path escapes project root: ${args.path}`;
|
|
60
74
|
if (!existsSync(filepath)) return `Error: File not found: ${args.path}`;
|
|
61
75
|
const content = readFileSync(filepath, 'utf8');
|
|
62
76
|
const count = content.split(args.old_string).length - 1;
|
|
@@ -90,7 +104,8 @@ function toolRunCommand(args) {
|
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
function toolListDirectory(args) {
|
|
93
|
-
const dirpath =
|
|
107
|
+
const dirpath = safePath(args.path || '.');
|
|
108
|
+
if (!dirpath) return `Error: Path escapes project root: ${args.path}`;
|
|
94
109
|
if (!existsSync(dirpath)) return `Error: Directory not found: ${args.path}`;
|
|
95
110
|
const entries = readdirSync(dirpath, { withFileTypes: true });
|
|
96
111
|
return entries.map(e => {
|
|
@@ -100,7 +115,8 @@ function toolListDirectory(args) {
|
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
function toolGlob(args) {
|
|
103
|
-
const base =
|
|
118
|
+
const base = safePath(args.path || '.');
|
|
119
|
+
if (!base) return `Error: Path escapes project root: ${args.path}`;
|
|
104
120
|
const regex = globToRegex(args.pattern);
|
|
105
121
|
const matches = [];
|
|
106
122
|
walkDir(base, base, regex, matches, 0);
|
|
@@ -445,6 +461,42 @@ function chatCompletion(messages) {
|
|
|
445
461
|
res.on('end', () => {
|
|
446
462
|
if (idleTimer) clearTimeout(idleTimer);
|
|
447
463
|
|
|
464
|
+
// Process any remaining data in buffer (API may not send trailing newline)
|
|
465
|
+
if (buffer.trim()) {
|
|
466
|
+
const trimmed = buffer.trim();
|
|
467
|
+
if (trimmed.startsWith('data: ')) {
|
|
468
|
+
const data = trimmed.slice(6);
|
|
469
|
+
if (data !== '[DONE]') {
|
|
470
|
+
try {
|
|
471
|
+
const parsed = JSON.parse(data);
|
|
472
|
+
if (parsed.usage) usage = parsed.usage;
|
|
473
|
+
const choice = parsed.choices && parsed.choices[0];
|
|
474
|
+
if (choice && choice.delta) {
|
|
475
|
+
if (choice.delta.content) {
|
|
476
|
+
content += choice.delta.content;
|
|
477
|
+
process.stdout.write(choice.delta.content);
|
|
478
|
+
}
|
|
479
|
+
if (choice.delta.tool_calls) {
|
|
480
|
+
for (const tc of choice.delta.tool_calls) {
|
|
481
|
+
const idx = tc.index;
|
|
482
|
+
if (!toolCalls.has(idx)) {
|
|
483
|
+
toolCalls.set(idx, { id: tc.id || '', name: '', arguments: '' });
|
|
484
|
+
}
|
|
485
|
+
const entry = toolCalls.get(idx);
|
|
486
|
+
if (tc.id) entry.id = tc.id;
|
|
487
|
+
if (tc.function) {
|
|
488
|
+
if (tc.function.name) entry.name = tc.function.name;
|
|
489
|
+
if (tc.function.arguments) entry.arguments += tc.function.arguments;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch { /* malformed final chunk — skip */ }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
buffer = '';
|
|
498
|
+
}
|
|
499
|
+
|
|
448
500
|
// Convert Map to sorted array
|
|
449
501
|
const sortedTools = [];
|
|
450
502
|
const indices = Array.from(toolCalls.keys()).sort((a, b) => a - b);
|