#!/usr/bin/env bash
# d.sh: Bash 4.3+ and curl, no Python, jq, Node or package installation.
# Usage: edit the configuration below, then run: bash d.sh 'Set up this project'
# Commands run with your permissions. Linux/macOS/WSL; text files only.
# Independent implementation inspired by deepseek-ai/deepseek-harness minimal.
# ---- User configuration -----------------------------------------------------
# Environment variables with the same names override these defaults.
# Connection
BASE_URL=${BASE_URL:-https://api.deepseek.com}
MODEL_NAME=${MODEL_NAME:-deepseek-v4-flash}
# Required. Never commit a real key.
DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-''}
# Runtime
DSH_CWD=${DSH_CWD:-'.'}
DSH_SESSION_FILE=${DSH_SESSION_FILE-'session.jsonl'} # empty = disabled
DSH_MAX_TOKENS=${DSH_MAX_TOKENS:-32768}
DSH_MAX_STEPS=${DSH_MAX_STEPS:-100}
DSH_COMMAND_TIMEOUT=${DSH_COMMAND_TIMEOUT:-300}
DSH_API_TIMEOUT=${DSH_API_TIMEOUT:-600}
DSH_SHOW_THINKING=${DSH_SHOW_THINKING:-0} # 0 / 1
DSH_STREAM=${DSH_STREAM:-1} # 0 / 1
DSH_REASONING_EFFORT=${DSH_REASONING_EFFORT:-'max'} # none / low / high / max
# Verbatim upstream sdk-minimal persona (MIT):
# deepseek-ai/deepseek-harness/packages/bundle/sdk-minimal/cordis.patch.yml
DSH_SYSTEM_PROMPT=${DSH_SYSTEM_PROMPT:-'You are a helpful software engineer assistant.'}
# Context compaction
DSH_CONTEXT_WINDOW=${DSH_CONTEXT_WINDOW:-1000000}
DSH_AUTO_COMPACT=${DSH_AUTO_COMPACT:-1} # 0 / 1
DSH_COMPACT_THRESHOLD=${DSH_COMPACT_THRESHOLD:-''} # empty = 80% of context
DSH_COMPACT_RETAIN=${DSH_COMPACT_RETAIN:-''} # empty = 16% of context
DSH_COMPACT_MAX_TOKENS=${DSH_COMPACT_MAX_TOKENS:-8192}
DSH_COMPACT_RETRIES=${DSH_COMPACT_RETRIES:-1}
DSH_MAX_OVERFLOW_RETRIES=${DSH_MAX_OVERFLOW_RETRIES:-1}
# ---- End user configuration -------------------------------------------------
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then
printf 'd.sh requires Bash 4.3+.\n' >&2
exit 1
fi
# `errexit` and `nounset` are intentionally disabled: tool failures are model
# input, and the JSON/SSE state machines use sparse arrays. Failures that cross
# a function boundary are checked explicitly instead.
readonly DSH_OUTPUT_LIMIT=16000
readonly DSH_CLIP_EDGE=7900
readonly DSH_JSON_MAX_DEPTH=64
readonly DSH_SHELL_READ_SIZE=4096
readonly DSH_STREAM_MAX_TOOL_INDEX=255
# Verbatim upstream compaction marker (MIT):
# deepseek-ai/deepseek-harness/packages/compaction/compaction-tool-result-pruner/src/config.ts
readonly DSH_PRUNE_THRESHOLD=8192
readonly DSH_PRUNE_HEAD=4096
readonly DSH_PRUNE_TAIL=1024
readonly DSH_PRUNE_MARKER=$'\n\n[... tool result middle pruned ...]\n\n'
DSH_PID=''
DSH_IN=''
DSH_OUT=''
DSH_ACTIVE_CURL_PID=''
DSH_TOOL_RUNNING=0
DSH_INTERRUPTED=0
# Large and structured return values use namespaced globals to avoid command
# substitutions, which would copy data and discard state changes in subshells.
declare -A DSH_VALUE DSH_TYPE DSH_RAW
declare -a DSH_MESSAGES DSH_ROLES
declare -a DSH_CALL_IDS DSH_CALL_NAMES DSH_CALL_ARGUMENTS
declare -a DSH_EDITOR_LINES
declare -a DSH_STREAM_TOOL_SEEN DSH_STREAM_TOOL_IDS
declare -a DSH_STREAM_TOOL_NAMES DSH_STREAM_TOOL_ARGUMENTS
# The checkpoint preamble and instruction below are copied verbatim (MIT) from:
# deepseek-ai/deepseek-harness/packages/compaction/compaction-basic/src/summarizer.ts
DSH_CHECKPOINT_PREAMBLE='This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.'
# Status text adapted from the upstream Web UI (MIT):
# deepseek-ai/deepseek-harness/packages/client/ui-chat/src/client/locale.ts
DSH_STATUS_RUNNING='[Deep diving ...]'
DSH_STATUS_COMPACTING='[Compacting context ...]'
IFS= read -r -d '' DSH_COMPACTION_INSTRUCTION <<'TEXT' || :
You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.
Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write "(none)" for an empty section — never drop a section.
## Primary Request and Intent
- [the user's original and evolving goals; quote verbatim where the exact wording matters]
## Key Technical Concepts
- [technologies, frameworks, patterns, and conventions in play]
## Files and Code
- [exact path: why it matters, key changes or snippets]
## Errors and Fixes
- [error: how it was resolved, plus any related user feedback]
## Pending Jobs
- [explicitly requested work not yet completed]
## Current Work
- [precisely what was in progress at this checkpoint]
## Next Step
- [the single next action, directly in line with the most recent request, or "(none)"]
## Critical Context
- [decisions and their rationale, constraints, user preferences, open questions, data needed to continue]
Rules:
- Write concise English engineering prose. Preserve exact file paths, commands, error strings, identifiers, numeric values, function signatures, and syntax fragments.
- Capture user feedback and explicit instructions faithfully, especially corrections.
- Do NOT mention this summarization request or that the context was compacted.
- Output only the checkpoint text: do not call any tool or take any other action.
- If the conversation already contains a block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.
TEXT
readonly DSH_CHECKPOINT_PREAMBLE DSH_COMPACTION_INSTRUCTION
readonly DSH_STATUS_RUNNING DSH_STATUS_COMPACTING
# ---- Diagnostics and setup --------------------------------------------------
dsh_log() { printf '%s\n' "$*" >&2; }
dsh_error() { DSH_RESULT="Tool error: $*"; return 1; }
dsh_is_positive_integer() { [[ $1 =~ ^[1-9][0-9]{0,8}$ ]]; }
dsh_is_nonnegative_integer() { [[ $1 =~ ^[0-9]{1,9}$ ]]; }
dsh_is_boolean() { [[ $1 == 0 || $1 == 1 ]]; }
dsh_prompt_read() {
local prompt=$1 secret=${2:-0}
if ((secret)); then
IFS= read -r -s -p "$prompt" DSH_PROMPT_VALUE || return 1
printf '\n' >&2
else
IFS= read -r -p "$prompt" DSH_PROMPT_VALUE || return 1
fi
}
dsh_configure_connection() {
local answer current_url=$BASE_URL current_model=$MODEL_NAME value
[[ ${DSH_INTERACTIVE_SETUP:-0} == 1 || -z $DEEPSEEK_API_KEY ]] || return 0
[[ -t 0 ]] || {
dsh_log 'Interactive setup requires terminal stdin.'
return 1
}
dsh_prompt_read 'Configure BASE_URL / MODEL_NAME / DEEPSEEK_API_KEY? [Y/n] ' || return 1
answer=${DSH_PROMPT_VALUE,,}
while :; do
case $answer in
''|y|yes) break ;;
n|no)
if [[ -z $DEEPSEEK_API_KEY ]]; then
dsh_log 'DEEPSEEK_API_KEY is required when interactive setup is skipped.'
return 1
fi
return 0
;;
*)
dsh_prompt_read 'Please answer Y or N [Y/n] ' || return 1
answer=${DSH_PROMPT_VALUE,,}
;;
esac
done
dsh_prompt_read "BASE_URL [$current_url]: " || return 1
value=$DSH_PROMPT_VALUE
[[ -z $value ]] || current_url=$value
dsh_prompt_read "MODEL_NAME [$current_model]: " || return 1
value=$DSH_PROMPT_VALUE
[[ -z $value ]] || current_model=$value
if [[ -n $DEEPSEEK_API_KEY ]]; then
dsh_prompt_read 'DEEPSEEK_API_KEY [press Enter to keep current]: ' 1 || return 1
value=$DSH_PROMPT_VALUE
[[ -z $value ]] || DEEPSEEK_API_KEY=$value
else
while [[ -z $DEEPSEEK_API_KEY ]]; do
dsh_prompt_read 'DEEPSEEK_API_KEY [required]: ' 1 || return 1
DEEPSEEK_API_KEY=$DSH_PROMPT_VALUE
done
fi
BASE_URL=$current_url
MODEL_NAME=$current_model
DSH_SETUP_CHANGED=1
}
dsh_persist_connection() {
local source=${BASH_SOURCE[0]} target=$PWD/d.sh tmp=$PWD/.d.sh.tmp.$$
local line quoted old_umask found_url=0 found_model=0 found_key=0
[[ ${DSH_SETUP_CHANGED:-0} == 1 ]] || return 0
case $source in
/*) ;;
*) source=$PWD/${source#./} ;;
esac
if [[ ! -f $source ]]; then
if [[ -f $target && -r $target ]]; then
source=$target
else
dsh_log 'Configuration is active for this run only.'
return 0
fi
fi
[[ -f $source && -r $source ]] || {
dsh_log 'Cannot save configuration: run a downloaded copy of d.sh instead of piping it into bash.'
return 1
}
if [[ ( -e $target || -L $target ) && $target != "$source" ]]; then
dsh_log "Cannot save configuration: $target already exists."
return 1
fi
old_umask=$(umask)
umask 077
if ! {
while IFS= read -r line || [[ -n $line ]]; do
case $line in
BASE_URL=*)
printf -v quoted '%q' "$BASE_URL"
printf "BASE_URL=\${BASE_URL:-%s}\n" "$quoted"
found_url=1 ;;
MODEL_NAME=*)
printf -v quoted '%q' "$MODEL_NAME"
printf "MODEL_NAME=\${MODEL_NAME:-%s}\n" "$quoted"
found_model=1 ;;
DEEPSEEK_API_KEY=*)
printf -v quoted '%q' "$DEEPSEEK_API_KEY"
printf "DEEPSEEK_API_KEY=\${DEEPSEEK_API_KEY:-%s}\n" "$quoted"
found_key=1 ;;
*) printf '%s\n' "$line" ;;
esac
done < "$source"
} > "$tmp"; then
umask "$old_umask"
rm -f -- "$tmp"
dsh_log "Cannot write configured script: $target"
return 1
fi
umask "$old_umask"
if ((found_url != 1 || found_model != 1 || found_key != 1)); then
rm -f -- "$tmp"
dsh_log 'Cannot save configuration: expected configuration lines were not found.'
return 1
fi
chmod 600 "$tmp" || {
rm -f -- "$tmp"
dsh_log "Cannot secure configured script: $target"
return 1
}
mv -f -- "$tmp" "$target" || {
rm -f -- "$tmp"
dsh_log "Cannot replace configured script: $target"
return 1
}
dsh_log "Saved configured script to $target (mode 600)."
}
dsh_validate_runtime_config() {
local name
case $DSH_REASONING_EFFORT in
none|low|high|max) ;;
*)
dsh_log 'DSH_REASONING_EFFORT must be none, low, high or max.'
return 1
;;
esac
for name in \
DSH_MAX_TOKENS \
DSH_MAX_STEPS \
DSH_COMMAND_TIMEOUT \
DSH_API_TIMEOUT \
DSH_CONTEXT_WINDOW; do
dsh_is_positive_integer "${!name}" || {
dsh_log "$name must be a positive integer."
return 1
}
done
dsh_is_boolean "$DSH_SHOW_THINKING" || {
dsh_log 'DSH_SHOW_THINKING must be 0 or 1.'
return 1
}
dsh_is_boolean "$DSH_STREAM" || {
dsh_log 'DSH_STREAM must be 0 or 1.'
return 1
}
dsh_is_boolean "$DSH_AUTO_COMPACT" || {
dsh_log 'DSH_AUTO_COMPACT must be 0 or 1.'
return 1
}
}
dsh_validate_compaction_config() {
local name
dsh_is_positive_integer "$DSH_COMPACT_MAX_TOKENS" || {
dsh_log 'DSH_COMPACT_MAX_TOKENS must be a positive integer.'
return 1
}
for name in DSH_COMPACT_RETRIES DSH_MAX_OVERFLOW_RETRIES; do
dsh_is_nonnegative_integer "${!name}" || {
dsh_log "$name must be a non-negative integer."
return 1
}
done
[[ -n $DSH_COMPACT_THRESHOLD ]] || DSH_COMPACT_THRESHOLD=$((DSH_CONTEXT_WINDOW * 80 / 100))
[[ -n $DSH_COMPACT_RETAIN ]] || DSH_COMPACT_RETAIN=$((DSH_CONTEXT_WINDOW * 16 / 100))
dsh_is_positive_integer "$DSH_COMPACT_THRESHOLD" || {
dsh_log 'DSH_COMPACT_THRESHOLD must be empty or a positive integer.'
return 1
}
dsh_is_nonnegative_integer "$DSH_COMPACT_RETAIN" || {
dsh_log 'DSH_COMPACT_RETAIN must be empty or a non-negative integer.'
return 1
}
((DSH_COMPACT_THRESHOLD < DSH_CONTEXT_WINDOW)) || {
dsh_log 'DSH_COMPACT_THRESHOLD must be smaller than DSH_CONTEXT_WINDOW.'
return 1
}
((DSH_COMPACT_RETAIN < DSH_COMPACT_THRESHOLD)) || {
dsh_log 'DSH_COMPACT_RETAIN must be smaller than DSH_COMPACT_THRESHOLD.'
return 1
}
}
dsh_validate_connection_config() {
[[ -n $MODEL_NAME ]] || {
dsh_log 'MODEL_NAME must not be empty.'
return 1
}
[[ -n $DEEPSEEK_API_KEY ]] || {
dsh_log 'DEEPSEEK_API_KEY must not be empty.'
return 1
}
[[ $DEEPSEEK_API_KEY != *[$'\r\n'\"\\]* ]] || {
dsh_log 'DEEPSEEK_API_KEY contains invalid characters.'
return 1
}
case $BASE_URL in
http://*|https://*) ;;
*)
dsh_log 'BASE_URL must start with http:// or https://.'
return 1
;;
esac
command -v curl >/dev/null || {
dsh_log 'curl is required because Bash builtins cannot establish HTTPS connections.'
return 1
}
}
dsh_validate_config() {
dsh_validate_runtime_config || return 1
dsh_validate_compaction_config || return 1
dsh_validate_connection_config
}
# ---- Text and JSON encoding -------------------------------------------------
dsh_clip() {
DSH_RESULT=$1
if (( ${#DSH_RESULT} > DSH_OUTPUT_LIMIT )); then
DSH_RESULT=${DSH_RESULT:0:DSH_CLIP_EDGE}$'\n\n'${DSH_RESULT: -DSH_CLIP_EDGE}
fi
}
# JSON strings are escaped/decoded using Bash builtins, including UTF-16
# surrogate pairs. NUL cannot be represented by Bash and is rejected explicitly.
dsh_quote() {
local text=$1 i char escape
text=${text//\\/\\\\}
text=${text//\"/\\\"}
for ((i=1; i<32; i++)); do
printf -v escape '\\%03o' "$i"
printf -v char '%b' "$escape"
printf -v escape '\\u%04x' "$i"
text=${text//"$char"/"$escape"}
done
DSH_QUOTED=\"$text\"
}
# ---- Conversation and persistence ------------------------------------------
dsh_history_rebuild() {
local message separator=''
DSH_HISTORY=''
for message in "${DSH_MESSAGES[@]}"; do
DSH_HISTORY+=$separator$message
separator=,
done
}
dsh_history_reset() {
dsh_quote "$DSH_SYSTEM_PROMPT"
DSH_MESSAGES=("{\"role\":\"system\",\"content\":$DSH_QUOTED}")
DSH_ROLES=(system)
dsh_history_rebuild
}
dsh_history_append() {
DSH_MESSAGES+=("$1")
DSH_ROLES+=("$2")
DSH_HISTORY+=",$1"
}
dsh_history_join() {
local start=$1 end=$2 i separator=
DSH_JOINED=
for ((i=start; i<=end; i++)); do
DSH_JOINED+=$separator${DSH_MESSAGES[i]}
separator=,
done
}
# Session file format: one "\t" line per message, system excluded.
# Rewritten atomically, so an interrupted run never leaves a half-written history.
dsh_session_save() {
[[ -n $DSH_SESSION_FILE && ${DSH_SESSION_READY:-0} == 1 ]] || return 0
local i tmp=$DSH_SESSION_FILE.tmp.$$ old_umask write_status=0
old_umask=$(umask)
umask 077
{
for ((i=1; i<${#DSH_MESSAGES[@]}; i++)); do
printf '%s\t%s\n' "${DSH_ROLES[i]}" "${DSH_MESSAGES[i]}"
done
} > "$tmp" || write_status=$?
umask "$old_umask"
if ((write_status != 0)); then
rm -f -- "$tmp"
dsh_log "Cannot write session file: $DSH_SESSION_FILE"
return 1
fi
chmod 600 "$tmp" || {
rm -f -- "$tmp"
dsh_log "Cannot secure session file: $DSH_SESSION_FILE"
return 1
}
mv -f -- "$tmp" "$DSH_SESSION_FILE" || {
rm -f -- "$tmp"
dsh_log "Cannot replace session file: $DSH_SESSION_FILE"
return 1
}
}
dsh_session_load() {
DSH_SESSION_LOADED=0
[[ -n $DSH_SESSION_FILE && -f $DSH_SESSION_FILE ]] || return 0
local role message skipped=0
while IFS=$'\t' read -r role message || [[ -n $role ]]; do
message=${message%$'\r'}
case $role in
system) continue ;;
user|assistant|tool) ;;
*) ((skipped+=1)); continue ;;
esac
if [[ -z $message ]] || ! dsh_parse "$message" ||
[[ ${DSH_TYPE[root]} != object || ${DSH_TYPE[root/role]} != string || ${DSH_VALUE[root/role]} != "$role" ]]; then
((skipped+=1))
continue
fi
DSH_MESSAGES+=("$message")
DSH_ROLES+=("$role")
((DSH_SESSION_LOADED+=1))
done < "$DSH_SESSION_FILE"
dsh_history_rebuild
((skipped)) && dsh_log "Ignored $skipped malformed line(s) in $DSH_SESSION_FILE."
return 0
}
# ---- JSON parser ------------------------------------------------------------
dsh_utf8() {
local n=$1 octets='' byte escaped
local -a bytes
(( n > 0 && n <= 0x10ffff && (n < 0xd800 || n > 0xdfff) )) || return 1
if ((n < 128)); then
bytes=("$n")
elif ((n < 2048)); then
bytes=("$((192 | n >> 6))" "$((128 | n & 63))")
elif ((n < 65536)); then
bytes=("$((224 | n >> 12))" "$((128 | n >> 6 & 63))" "$((128 | n & 63))")
else
bytes=("$((240 | n >> 18))" "$((128 | n >> 12 & 63))" "$((128 | n >> 6 & 63))" "$((128 | n & 63))")
fi
for byte in "${bytes[@]}"; do
printf -v escaped '\\%03o' "$byte"
octets+=$escaped
done
printf -v DSH_CHAR '%b' "$octets"
}
dsh_json_string() {
local value='' part escape hex low n
[[ ${DSH_INPUT:0:1} == '"' ]] || return 1
DSH_INPUT=${DSH_INPUT:1}
while [[ -n $DSH_INPUT ]]; do
part=${DSH_INPUT%%[\"\\]*}
[[ $part != *[$'\001'-$'\037']* ]] || return 1
value+=$part
DSH_INPUT=${DSH_INPUT:${#part}}
case ${DSH_INPUT:0:1} in
'"')
DSH_INPUT=${DSH_INPUT:1}
DSH_STRING=$value
return 0
;;
\\)
escape=${DSH_INPUT:1:1}
DSH_INPUT=${DSH_INPUT:2}
case $escape in
'"'|\\|'/') value+=$escape ;;
b) value+=$'\b' ;;
f) value+=$'\f' ;;
n) value+=$'\n' ;;
r) value+=$'\r' ;;
t) value+=$'\t' ;;
u)
hex=${DSH_INPUT:0:4}
[[ $hex =~ ^[0-9a-fA-F]{4}$ ]] || return 1
DSH_INPUT=${DSH_INPUT:4}
n=$((16#$hex))
if ((n >= 0xd800 && n <= 0xdbff)); then
[[ ${DSH_INPUT:0:2} == '\u' ]] || return 1
low=${DSH_INPUT:2:4}
[[ $low =~ ^[0-9a-fA-F]{4}$ ]] || return 1
((16#$low >= 0xdc00 && 16#$low <= 0xdfff)) || return 1
n=$((0x10000 + (n - 0xd800) * 1024 + 16#$low - 0xdc00))
DSH_INPUT=${DSH_INPUT:6}
fi
dsh_utf8 "$n" || return 1
value+=$DSH_CHAR
;;
*) return 1 ;;
esac
;;
*) return 1 ;;
esac
done
return 1
}
dsh_json_space() { DSH_INPUT=${DSH_INPUT#"${DSH_INPUT%%[!$' \t\r\n']*}"}; }
dsh_json_value() {
local path=$1 depth=$2 original key index=0 close token
((depth <= DSH_JSON_MAX_DEPTH)) || return 1
dsh_json_space
original=$DSH_INPUT
case ${DSH_INPUT:0:1} in
'"')
dsh_json_string || return 1
DSH_VALUE[$path]=$DSH_STRING
DSH_TYPE[$path]=string
;;
'{'|'[')
if [[ ${DSH_INPUT:0:1} == '{' ]]; then
close='}'
DSH_TYPE[$path]=object
else
close=']'
DSH_TYPE[$path]=array
fi
DSH_INPUT=${DSH_INPUT:1}
dsh_json_space
if [[ ${DSH_INPUT:0:1} != "$close" ]]; then
while :; do
if [[ $close == '}' ]]; then
dsh_json_string || return 1
key=$DSH_STRING
dsh_json_space
[[ ${DSH_INPUT:0:1} == ':' ]] || return 1
DSH_INPUT=${DSH_INPUT:1}
else
key=$index
((index+=1))
fi
dsh_json_value "$path/$key" "$((depth+1))" || return 1
dsh_json_space
[[ ${DSH_INPUT:0:1} == ',' ]] || break
DSH_INPUT=${DSH_INPUT:1}; dsh_json_space
done
fi
[[ ${DSH_INPUT:0:1} == "$close" ]] || return 1
DSH_INPUT=${DSH_INPUT:1}
;;
*)
if [[ $DSH_INPUT =~ ^(true|false|null|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?) ]]; then
token=${BASH_REMATCH[0]}
DSH_INPUT=${DSH_INPUT:${#token}}
DSH_VALUE[$path]=$token
DSH_TYPE[$path]=$token
[[ $token == true || $token == false || $token == null ]] || DSH_TYPE[$path]=number
else
return 1
fi
;;
esac
DSH_RAW[$path]=${original:0:${#original}-${#DSH_INPUT}}
}
dsh_parse() {
DSH_VALUE=()
DSH_TYPE=()
DSH_RAW=()
DSH_INPUT=$1
dsh_json_value root 0 || return 1
dsh_json_space
[[ -z $DSH_INPUT ]]
}
# ---- Persistent shell -------------------------------------------------------
dsh_shell_close() {
if [[ -n $DSH_PID ]]; then
# Job control gives this owned coprocess its own process group.
kill -KILL -- "-$DSH_PID" 2>/dev/null || :
wait "$DSH_PID" 2>/dev/null || :
fi
[[ -z $DSH_IN ]] || exec {DSH_IN}>&-
[[ -z $DSH_OUT ]] || exec {DSH_OUT}<&-
DSH_PID=''
DSH_IN=''
DSH_OUT=''
}
dsh_cancel_active_curl() {
[[ -n $DSH_ACTIVE_CURL_PID ]] || return 0
# SIGKILL, not SIGINT: background children may have SIGINT ignored.
kill -KILL -- "$DSH_ACTIVE_CURL_PID" 2>/dev/null || :
}
dsh_interrupt() {
DSH_INTERRUPTED=1
dsh_cancel_active_curl
if ((DSH_TOOL_RUNNING)); then
dsh_shell_close
fi
printf '\n' >&2
}
dsh_shell_start() {
set -m
coproc DSH_SHELL {
unset BASH_ENV
exec bash --noprofile --norc -c '
while IFS= read -r -d "" __mini_tag && IFS= read -r -d "" __mini_command; do
eval -- "$__mini_command" &1
}
DSH_PID=$DSH_SHELL_PID
exec {DSH_IN}>&"${DSH_SHELL[1]}" {DSH_OUT}<&"${DSH_SHELL[0]}"
}
dsh_bash() {
local command=$1 token marker output='' chunk status rest started=$SECONDS
local reset=$'\n[shell reset; next call starts in the initial directory/environment]'
[[ -n $command ]] || {
dsh_error 'command must not be empty'
return 1
}
if ((DSH_INTERRUPTED)); then
DSH_RESULT='[interrupted by user; command not started]'
return 0
fi
if [[ -n $DSH_PID ]] && ! kill -0 "$DSH_PID" 2>/dev/null; then
dsh_shell_close
fi
[[ -n $DSH_PID ]] || dsh_shell_start
if ((DSH_INTERRUPTED)); then
dsh_shell_close
DSH_RESULT='[interrupted by user; command not started]'
return 0
fi
printf -v token '%04x%04x%04x%04x' "$RANDOM" "$RANDOM" "$RANDOM" "$RANDOM"
marker=$'\036'$token:
if ! printf '%s\0%s\0' "$token" "$command" >&"$DSH_IN"; then
dsh_shell_close
dsh_error "shell input closed$reset"
return 1
fi
while ((SECONDS - started < DSH_COMMAND_TIMEOUT)); do
chunk=
if IFS= read -r -t 0.1 -N "$DSH_SHELL_READ_SIZE" chunk <&"$DSH_OUT"; then
status=0
else
status=$?
fi
output+=$chunk
if [[ $output == *"$marker"* ]]; then
rest=${output##*"$marker"}
if [[ $rest == *$'\037'* ]]; then
rest=${rest%%$'\037'*}
if [[ $rest =~ ^[0-9]+$ ]]; then
dsh_clip "${output%%"$marker"*}"
DSH_RESULT+=$'\n'"[exit code: $rest]"
return 0
fi
fi
fi
if (( ${#output} > DSH_OUTPUT_LIMIT * 2 )); then
output=${output:0:DSH_OUTPUT_LIMIT}$'\n\n'${output: -DSH_OUTPUT_LIMIT}
fi
if ((status == 1)) || ! kill -0 "$DSH_PID" 2>/dev/null; then
dsh_shell_close
dsh_clip "$output"
if ((DSH_INTERRUPTED)); then
DSH_RESULT+=$'\n[interrupted by user]'"$reset"
else
DSH_RESULT+=$'\n[shell exited]'"$reset"
fi
return 0
fi
done
dsh_shell_close
dsh_clip "$output"
DSH_RESULT+=$'\n[command timed out]'"$reset"
}
# ---- File editor ------------------------------------------------------------
# Tool arguments are parsed into DSH_VALUE/DSH_TYPE. All editor operations use
# builtins; commands requested by the model may of course use installed tools.
dsh_editor_read_file() {
local path=$1
[[ -f $path && -r $path ]] || {
dsh_error 'path must be an existing readable regular file'
return 1
}
DSH_EDITOR_TEXT=''
if IFS= read -r -d '' DSH_EDITOR_TEXT < "$path"; then
dsh_error 'NUL/binary files are unsupported'
return 1
fi
}
dsh_editor_split_lines() {
local rest=$DSH_EDITOR_TEXT
DSH_EDITOR_LINES=()
while [[ $rest == *$'\n'* ]]; do
DSH_EDITOR_LINES+=("${rest%%$'\n'*}")
rest=${rest#*$'\n'}
done
DSH_EDITOR_LINES+=("$rest")
}
dsh_editor_list_directory() {
local path=$1 child grandchild
[[ -z ${DSH_TYPE[root/view_range]} || ${DSH_TYPE[root/view_range]} == null ]] || {
dsh_error 'view_range is only valid for files'
return 1
}
DSH_RESULT="$path/"
for child in "$path"/*; do
[[ -e $child || -L $child ]] || continue
[[ ${child##*/} != node_modules && ${child##*/} != __pycache__ ]] || continue
DSH_RESULT+=$'\n'"$child"
if [[ -d $child && ! -L $child ]]; then
for grandchild in "$child"/*; do
[[ -e $grandchild || -L $grandchild ]] || continue
[[ ${grandchild##*/} != node_modules && ${grandchild##*/} != __pycache__ ]] || continue
DSH_RESULT+=$'\n'"$grandchild"
((${#DSH_RESULT} <= DSH_OUTPUT_LIMIT)) || break
done
fi
if ((${#DSH_RESULT} > DSH_OUTPUT_LIMIT)); then
dsh_clip "$DSH_RESULT"
return 0
fi
done
}
dsh_editor_create() {
local path=$1
[[ ${DSH_TYPE[root/file_text]} == string ]] || {
dsh_error 'file_text must be a string'
return 1
}
[[ ! -e $path && ! -L $path ]] || {
dsh_error 'create cannot overwrite an existing path'
return 1
}
if (set -o noclobber; printf '%s' "${DSH_VALUE[root/file_text]}" > "$path"); then
DSH_RESULT="Created $path"
else
dsh_error "cannot create $path"
fi
}
dsh_editor_view_file() {
local start=1 end=${#DSH_EDITOR_LINES[@]} row i
if [[ -n ${DSH_TYPE[root/view_range]} && ${DSH_TYPE[root/view_range]} != null ]]; then
start=${DSH_VALUE[root/view_range/0]}
end=${DSH_VALUE[root/view_range/1]}
if [[ ${DSH_TYPE[root/view_range]} != array ||
${DSH_TYPE[root/view_range/0]} != number ||
${DSH_TYPE[root/view_range/1]} != number ||
! $start =~ ^[1-9][0-9]{0,8}$ ||
! $end =~ ^(-1|[1-9][0-9]{0,8})$ ||
-n ${DSH_TYPE[root/view_range/2]} ]]; then
dsh_error 'view_range must be [start, end], with end=-1 for EOF'
return 1
fi
fi
((end != -1)) || end=${#DSH_EDITOR_LINES[@]}
((start <= end && end <= ${#DSH_EDITOR_LINES[@]})) || {
dsh_error 'view_range is outside the file'
return 1
}
DSH_RESULT=''
for ((i=start; i<=end; i++)); do
printf -v row '%6d %s\n' "$i" "${DSH_EDITOR_LINES[i-1]}"
DSH_RESULT+=$row
if ((${#DSH_RESULT} > DSH_OUTPUT_LIMIT)); then
dsh_clip "$DSH_RESULT"
return 0
fi
done
}
dsh_editor_replace() {
local path=$1 old=${DSH_VALUE[root/old_str]} new rest output
[[ ${DSH_TYPE[root/old_str]} == string && -n $old ]] || {
dsh_error 'old_str must be a non-empty string'
return 1
}
[[ -z ${DSH_TYPE[root/new_str]} || ${DSH_TYPE[root/new_str]} == string ]] || {
dsh_error 'new_str must be a string or omitted, not null'
return 1
}
new=${DSH_VALUE[root/new_str]}
[[ $DSH_EDITOR_TEXT == *"$old"* ]] || { dsh_error 'old_str did not match'; return 1; }
rest=${DSH_EDITOR_TEXT#*"$old"}
[[ $rest != *"$old"* ]] || {
dsh_error 'old_str matched multiple times; include more context'
return 1
}
output=${DSH_EDITOR_TEXT%%"$old"*}$new$rest
if printf '%s' "$output" > "$path"; then
DSH_RESULT="Edited $path"
else
dsh_error "cannot write $path"
fi
}
dsh_editor_insert() {
local path=$1 line=${DSH_VALUE[root/insert_line]} new=${DSH_VALUE[root/new_str]} output
if [[ ${DSH_TYPE[root/new_str]} != string ||
${DSH_TYPE[root/insert_line]} != number ||
! $line =~ ^(0|[1-9][0-9]{0,8})$ ]]; then
dsh_error 'insert requires integer insert_line and string new_str'
return 1
fi
((line <= ${#DSH_EDITOR_LINES[@]})) || {
dsh_error 'insert_line is outside the file'
return 1
}
DSH_EDITOR_LINES=("${DSH_EDITOR_LINES[@]:0:line}" "$new" "${DSH_EDITOR_LINES[@]:line}")
printf -v output '%s\n' "${DSH_EDITOR_LINES[@]}"
output=${output%$'\n'}
if printf '%s' "$output" > "$path"; then
DSH_RESULT="Edited $path"
else
dsh_error "cannot write $path"
fi
}
dsh_editor() {
local command=${DSH_VALUE[root/command]} path=${DSH_VALUE[root/path]}
[[ $path == /* ]] || { dsh_error 'path must be absolute'; return 1; }
case $command in
create) dsh_editor_create "$path" ;;
view)
if [[ -d $path ]]; then
dsh_editor_list_directory "$path"
else
dsh_editor_read_file "$path" || return 1
dsh_editor_split_lines
dsh_editor_view_file
fi
;;
str_replace)
dsh_editor_read_file "$path" || return 1
dsh_editor_replace "$path"
;;
insert)
dsh_editor_read_file "$path" || return 1
dsh_editor_split_lines
dsh_editor_insert "$path"
;;
*) dsh_error "unknown editor command: $command" ;;
esac
}
# ---- Tool schemas -----------------------------------------------------------
dsh_tools() {
# read -d '' preserves literal newlines in this embedded JSON schema.
# Descriptions are adapted from the upstream sdk-minimal persistent Bash and
# str_replace_editor schemas; wording reflects this script's different runtime.
# Source (MIT): deepseek-ai/deepseek-harness/packages/bundle/sdk-minimal/cordis.patch.yml
# Source (MIT): deepseek-ai/deepseek-harness/packages/fs/tool-str-replace-editor/src/index.ts
IFS= read -r -d '' DSH_TOOLS <<'JSON' || :
[
{"type":"function","function":{
"name":"bash",
"description":"Run a command in a persistent Bash shell. Current directory, environment variables, functions and sourced virtual environments persist across calls and user turns. Network access and permissions are those of the host. No TTY; command stdin is /dev/null. Use noninteractive installer flags. Start long-lived processes in the background with output redirected to log files. Timeout resets the shell and its environment. Avoid very large output.",
"parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}
}},
{"type":"function","function":{
"name":"str_replace_editor",
"description":"View, create or edit text files using absolute paths. view shows numbered lines or lists a directory two levels deep, excluding hidden entries, node_modules and __pycache__. create never overwrites an existing path. str_replace requires old_str to match exactly once, including whitespace; omit new_str to delete (null is invalid). insert adds new_str AFTER insert_line, where 0 means before the first line. Unused nullable parameters are ignored. Output is clipped at 16000 characters. NUL/binary files are unsupported.",
"parameters":{"type":"object","properties":{
"command":{"type":"string","enum":["view","create","str_replace","insert"]},
"path":{"type":"string","description":"Absolute path"},
"file_text":{"type":["string","null"]},
"old_str":{"type":["string","null"]},
"new_str":{"type":["string","null"]},
"insert_line":{"type":["integer","null"]},
"view_range":{"type":["array","null"],"items":{"type":"integer"},"description":"Inclusive [start,end], 1-based; end=-1 means EOF"}
},"required":["command","path"]}
}}
]
JSON
}
# ---- Context management and API transport ----------------------------------
dsh_estimate_json_tokens() {
DSH_TOKEN_ESTIMATE=$(((${#1} + 3) / 4 + 4))
}
dsh_history_estimate() {
local message total i
dsh_estimate_json_tokens "$DSH_TOOLS"
total=$DSH_TOKEN_ESTIMATE
for ((i=0; i<${#DSH_MESSAGES[@]}; i++)); do
message=${DSH_MESSAGES[i]}
dsh_estimate_json_tokens "$message"
((total+=DSH_TOKEN_ESTIMATE))
done
DSH_HISTORY_TOKENS=$total
}
dsh_prune_tool_results() {
local i content id pruned quoted_id quoted_content
DSH_PRUNE_COUNT=0
for ((i=1; i<${#DSH_MESSAGES[@]}; i++)); do
[[ ${DSH_ROLES[i]} == tool ]] || continue
if ! dsh_parse "${DSH_MESSAGES[i]}" || [[ ${DSH_TYPE[root/content]} != string ]]; then
dsh_log '[compact] Cannot parse a stored tool result; skipping pruning.'
continue
fi
content=${DSH_VALUE[root/content]}
((${#content} > DSH_PRUNE_THRESHOLD)) || continue
id=${DSH_VALUE[root/tool_call_id]}
pruned=${content:0:DSH_PRUNE_HEAD}$DSH_PRUNE_MARKER${content: -DSH_PRUNE_TAIL}
dsh_quote "$id"
quoted_id=$DSH_QUOTED
dsh_quote "$pruned"
quoted_content=$DSH_QUOTED
DSH_MESSAGES[i]="{\"role\":\"tool\",\"tool_call_id\":$quoted_id,\"content\":$quoted_content}"
((DSH_PRUNE_COUNT+=1))
done
if ((DSH_PRUNE_COUNT)); then
dsh_history_rebuild
dsh_log "[compact] Pruned $DSH_PRUNE_COUNT oversized tool result(s)."
fi
}
dsh_api_request() {
local payload=$1 body_file status_file response status
DSH_RESPONSE='' DSH_HTTP_STATUS='' DSH_CURL_STATUS=0
body_file=$(mktemp "${TMPDIR:-/tmp}/dsh-body.XXXXXX") || {
dsh_log 'Cannot create a temporary file for the API response.'
return 1
}
status_file=$(mktemp "${TMPDIR:-/tmp}/dsh-status.XXXXXX") || {
dsh_log 'Cannot create a temporary file for the API status.'
rm -f -- "$body_file"
return 1
}
# Feed headers through curl's stdin config so the key is not a process argument.
# Process substitution avoids a temporary request-body file. The response goes
# to temp files so a trapped SIGINT can kill curl by PID and reap it.
command curl --disable --silent --show-error --connect-timeout 30 --max-time "$DSH_API_TIMEOUT" \
--request POST --header 'Content-Type: application/json' \
--data-binary @<(printf '%s' "$payload") --output "$body_file" --write-out '%{http_code}' \
--config - "$BASE_URL" >"$status_file" <<< "header = \"Authorization: Bearer $DEEPSEEK_API_KEY\"" &
DSH_ACTIVE_CURL_PID=$!
((DSH_INTERRUPTED)) && dsh_cancel_active_curl
wait "$DSH_ACTIVE_CURL_PID"
DSH_CURL_STATUS=$?
if ((DSH_INTERRUPTED)); then
wait "$DSH_ACTIVE_CURL_PID" 2>/dev/null || :
DSH_ACTIVE_CURL_PID=''
rm -f -- "$body_file" "$status_file"
dsh_log '[interrupted] Request cancelled.'
return 130
fi
DSH_ACTIVE_CURL_PID=''
response=$(<"$body_file")
status=$(<"$status_file")
rm -f -- "$body_file" "$status_file"
((DSH_CURL_STATUS == 0)) || return 1
DSH_HTTP_STATUS=$status
DSH_RESPONSE=$response
}
dsh_build_payload() {
local messages=$1 streaming=$2 max_tokens=$3
local thinking=enabled effort='' model
[[ $DSH_REASONING_EFFORT != none ]] || thinking=disabled
[[ $DSH_REASONING_EFFORT == none ]] || effort=",\"reasoning_effort\":\"$DSH_REASONING_EFFORT\""
dsh_quote "$MODEL_NAME"
model=$DSH_QUOTED
DSH_PAYLOAD="{\"model\":$model,\"messages\":[$messages],\"tools\":$DSH_TOOLS,\"stream\":$streaming,\"max_tokens\":$max_tokens,\"thinking\":{\"type\":\"$thinking\"}$effort}"
}
dsh_report_http_error() {
local label=$1 status=$2 body=$3
body=${body//"$DEEPSEEK_API_KEY"/[redacted]}
if dsh_is_context_overflow "$body"; then
dsh_log "$label HTTP ${status:-unknown} reported a context-window overflow."
return 3
fi
dsh_log "$label HTTP ${status:-unknown}: ${body:0:2000}"
return 1
}
dsh_is_context_overflow() {
local detail=${1,,}
[[ $detail == *context_length_exceeded* || $detail == *context_window_exceeded* ||
$detail == *context-window-overflowed* || $detail == *'maximum context length'* ||
$detail == *'maximum context window'* || $detail == *'max context length'* ||
$detail == *'max context window'* || $detail == *'input is too long for this model'* ||
$detail == *'request too large for model context'* ||
$detail == *'input exceeds the model context'* ||
$detail == *'prompt exceeds the model context'* ||
$detail == *'messages exceed the model context'* ]]
}
dsh_summary_request() {
local selected=$1 messages response status instruction compact code
dsh_quote "$DSH_COMPACTION_INSTRUCTION"
instruction=$DSH_QUOTED
messages="${DSH_MESSAGES[0]},$selected,{\"role\":\"user\",\"content\":$instruction}"
dsh_build_payload "$messages" false "$DSH_COMPACT_MAX_TOKENS"
dsh_log "$DSH_STATUS_COMPACTING"
dsh_api_request "$DSH_PAYLOAD"
code=$?
if ((code != 0)); then
((code == 130)) && return 130
dsh_log "Compaction request failed (curl exit code $DSH_CURL_STATUS)."
return 1
fi
response=$DSH_RESPONSE
status=$DSH_HTTP_STATUS
if [[ $status != 200 ]]; then
response=${response//"$DEEPSEEK_API_KEY"/[redacted]}
dsh_log "Compaction API HTTP $status: ${response:0:2000}"
return 1
fi
dsh_parse "$response" || {
dsh_log 'Compaction API returned invalid or unsupported JSON.'
return 1
}
status=${DSH_VALUE[root/choices/0/finish_reason]}
[[ $status == stop ]] || {
dsh_log "Compaction did not finish cleanly (finish_reason=$status)."
return 1
}
[[ ${DSH_TYPE[root/choices/0/message]} == object &&
${DSH_VALUE[root/choices/0/message/role]} == assistant &&
${DSH_TYPE[root/choices/0/message/content]} == string &&
( -z ${DSH_TYPE[root/choices/0/message/tool_calls/0]} || ${DSH_TYPE[root/choices/0/message/tool_calls/0]} == null ) ]] || {
dsh_log 'Compaction response is missing a text-only assistant summary.'
return 1
}
DSH_SUMMARY=${DSH_VALUE[root/choices/0/message/content]}
compact=${DSH_SUMMARY//[[:space:]]/}
[[ -n $compact ]] || {
dsh_log 'Compaction produced an empty summary.'
return 1
}
}
dsh_select_compaction() {
local retain=$1 count=${#DSH_MESSAGES[@]} keep accumulated=0 i
((count > 2)) || return 1
keep=$count
for ((i=count-1; i>=1; i--)); do
dsh_estimate_json_tokens "${DSH_MESSAGES[i]}"
((accumulated+=DSH_TOKEN_ESTIMATE))
keep=$i
((accumulated >= retain)) && break
done
while ((keep > 1)) && [[ ${DSH_ROLES[keep]} == tool ]]; do
((keep-=1))
done
((keep > 1)) || return 1
DSH_COMPACT_KEEP=$keep
DSH_COMPACT_COUNT=$((keep - 1))
DSH_SHADOWED_TOKENS=0
for ((i=1; i\n'$DSH_SUMMARY$'\n'
dsh_quote "$checkpoint"
checkpoint_message="{\"role\":\"user\",\"content\":$DSH_QUOTED}"
dsh_estimate_json_tokens "$checkpoint_message"
checkpoint_tokens=$DSH_TOKEN_ESTIMATE
if ((checkpoint_tokens >= DSH_SHADOWED_TOKENS)); then
dsh_log "[compact] Summary is not smaller than its source (~$checkpoint_tokens >= ~$DSH_SHADOWED_TOKENS tokens); history unchanged."
return 2
fi
count=${#DSH_MESSAGES[@]}
messages=("${DSH_MESSAGES[0]}" "$checkpoint_message")
roles=(system user)
for ((i=DSH_COMPACT_KEEP; i ~$checkpoint_tokens tokens)."
if [[ $mode == manual ]]; then
dsh_session_save || return 2
fi
return 0
}
dsh_compact_if_needed() {
local attempt status
((DSH_AUTO_COMPACT)) || return 0
dsh_history_estimate
((DSH_HISTORY_TOKENS >= DSH_COMPACT_THRESHOLD)) || return 0
dsh_log "[compact] Estimated context ~$DSH_HISTORY_TOKENS tokens reached threshold $DSH_COMPACT_THRESHOLD."
dsh_prune_tool_results
dsh_history_estimate
((DSH_HISTORY_TOKENS >= DSH_COMPACT_THRESHOLD)) || return 0
for ((attempt=0; attempt<=DSH_COMPACT_RETRIES; attempt++)); do
dsh_compact_once pressure
status=$?
if ((status == 130)); then
return 130
fi
if ((status != 0)); then
dsh_log '[compact] Automatic compaction could not reduce the history further; continuing with the current history.'
return 0
fi
dsh_history_estimate
((DSH_HISTORY_TOKENS < DSH_COMPACT_THRESHOLD)) && return 0
done
dsh_log "[compact] Context remains above threshold (~$DSH_HISTORY_TOKENS tokens)."
}
dsh_compact_after_overflow() {
local status pruned
dsh_prune_tool_results
pruned=$DSH_PRUNE_COUNT
dsh_compact_once overflow
status=$?
((status == 130)) && return 130
((status == 0 || pruned > 0))
}
# ---- Streaming completion --------------------------------------------------
dsh_stream_reset() {
DSH_STREAM_ROLE=assistant
DSH_STREAM_CONTENT=''
DSH_STREAM_REASONING=''
DSH_STREAM_FINISH=''
DSH_STREAM_ERROR=''
DSH_STREAM_ERROR_BODY=''
DSH_STREAM_CONTENT_SEEN=0
DSH_STREAM_REASONING_SEEN=0
DSH_STREAM_MAX_TOOL=-1
DSH_STREAM_TOOL_SEEN=()
DSH_STREAM_TOOL_IDS=()
DSH_STREAM_TOOL_NAMES=()
DSH_STREAM_TOOL_ARGUMENTS=()
}
dsh_stream_consume_tool_call() {
local path=$1 index id
index=${DSH_VALUE[$path/index]}
if [[ ${DSH_TYPE[$path/index]} != number || ! $index =~ ^[0-9]+$ || $index -gt DSH_STREAM_MAX_TOOL_INDEX ]]; then
DSH_STREAM_ERROR='API returned an invalid streaming tool-call index.'
return 1
fi
DSH_STREAM_TOOL_SEEN[index]=1
((index > DSH_STREAM_MAX_TOOL)) && DSH_STREAM_MAX_TOOL=$index
if [[ ${DSH_TYPE[$path/id]} == string ]]; then
id=${DSH_VALUE[$path/id]}
if [[ -n ${DSH_STREAM_TOOL_IDS[index]} && ${DSH_STREAM_TOOL_IDS[index]} != "$id" ]]; then
DSH_STREAM_ERROR='API changed a streaming tool-call id.'
return 1
fi
DSH_STREAM_TOOL_IDS[index]=$id
fi
if [[ ${DSH_TYPE[$path/type]} == string && ${DSH_VALUE[$path/type]} != function ]]; then
DSH_STREAM_ERROR='API returned an unsupported streaming tool-call type.'
return 1
fi
if [[ ${DSH_TYPE[$path/function/name]} == string ]]; then
DSH_STREAM_TOOL_NAMES[index]+=${DSH_VALUE[$path/function/name]}
fi
if [[ ${DSH_TYPE[$path/function/arguments]} == string ]]; then
DSH_STREAM_TOOL_ARGUMENTS[index]+=${DSH_VALUE[$path/function/arguments]}
fi
}
dsh_stream_consume_event() {
local data=$1 path piece i
if ! dsh_parse "$data"; then
DSH_STREAM_ERROR='API returned an invalid or unsupported SSE JSON chunk.'
return 1
fi
if [[ ${DSH_TYPE[root/error]} == object ]]; then
DSH_STREAM_ERROR_BODY+="${DSH_STREAM_ERROR_BODY:+$'\n'}$data"
return 0
fi
[[ ${DSH_TYPE[root/choices/0]} == object ]] || return 0
if [[ ${DSH_TYPE[root/choices/0/delta/role]} == string ]]; then
DSH_STREAM_ROLE=${DSH_VALUE[root/choices/0/delta/role]}
fi
if [[ ${DSH_TYPE[root/choices/0/delta/reasoning_content]} == string ]]; then
piece=${DSH_VALUE[root/choices/0/delta/reasoning_content]}
DSH_STREAM_REASONING+=$piece
DSH_STREAM_REASONING_SEEN=1
((DSH_SHOW_THINKING)) && printf '%s' "$piece" >&2
fi
if [[ ${DSH_TYPE[root/choices/0/delta/content]} == string ]]; then
piece=${DSH_VALUE[root/choices/0/delta/content]}
DSH_STREAM_CONTENT+=$piece
DSH_STREAM_CONTENT_SEEN=1
printf '%s' "$piece"
fi
for ((i=0; ; i++)); do
path=root/choices/0/delta/tool_calls/$i
[[ -n ${DSH_TYPE[$path]} ]] || break
dsh_stream_consume_tool_call "$path" || return 1
done
if [[ ${DSH_TYPE[root/choices/0/finish_reason]} == string ]]; then
DSH_STREAM_FINISH=${DSH_VALUE[root/choices/0/finish_reason]}
fi
}
dsh_stream_build_response() {
local assistant content_json quoted_finish quoted_id quoted_name quoted_arguments
local tool_calls='' separator='' i
[[ $DSH_STREAM_ROLE == assistant ]] || {
dsh_log "API stream returned an unexpected role: $DSH_STREAM_ROLE"
return 1
}
[[ $DSH_STREAM_FINISH == stop || $DSH_STREAM_FINISH == tool_calls ]] || {
dsh_log "Incomplete API stream (finish_reason=$DSH_STREAM_FINISH). If length, increase DSH_MAX_TOKENS."
return 1
}
if ((DSH_STREAM_CONTENT_SEEN)); then
dsh_quote "$DSH_STREAM_CONTENT"
content_json=$DSH_QUOTED
else
content_json=null
fi
assistant="{\"role\":\"assistant\",\"content\":$content_json"
if ((DSH_STREAM_REASONING_SEEN)); then
dsh_quote "$DSH_STREAM_REASONING"
assistant+=",\"reasoning_content\":$DSH_QUOTED"
fi
if ((DSH_STREAM_MAX_TOOL >= 0)); then
for ((i=0; i<=DSH_STREAM_MAX_TOOL; i++)); do
[[ ${DSH_STREAM_TOOL_SEEN[i]} == 1 && -n ${DSH_STREAM_TOOL_IDS[i]} && -n ${DSH_STREAM_TOOL_NAMES[i]} ]] || {
dsh_log 'API returned an incomplete streaming tool call.'
return 1
}
dsh_quote "${DSH_STREAM_TOOL_IDS[i]}"
quoted_id=$DSH_QUOTED
dsh_quote "${DSH_STREAM_TOOL_NAMES[i]}"
quoted_name=$DSH_QUOTED
dsh_quote "${DSH_STREAM_TOOL_ARGUMENTS[i]}"
quoted_arguments=$DSH_QUOTED
tool_calls+=$separator"{\"id\":$quoted_id,\"type\":\"function\",\"function\":{\"name\":$quoted_name,\"arguments\":$quoted_arguments}}"
separator=,
done
assistant+=",\"tool_calls\":[$tool_calls]"
fi
assistant+='}'
dsh_quote "$DSH_STREAM_FINISH"
quoted_finish=$DSH_QUOTED
DSH_RESPONSE="{\"choices\":[{\"finish_reason\":$quoted_finish,\"message\":$assistant}]}"
dsh_parse "$DSH_RESPONSE" || {
dsh_log 'Failed to assemble the streamed assistant message.'
return 1
}
dsh_history_append "$assistant" assistant
}
dsh_complete_stream() {
local stream_fd curl_pid curl_status line data http_status=''
dsh_stream_reset
dsh_build_payload "$DSH_HISTORY" true "$DSH_MAX_TOKENS"
dsh_log "$DSH_STATUS_RUNNING"
exec {stream_fd}< <(
command curl --disable --silent --show-error --no-buffer --connect-timeout 30 --max-time "$DSH_API_TIMEOUT" \
--request POST --header 'Content-Type: application/json' --header 'Accept: text/event-stream' \
--data-binary @<(printf '%s' "$DSH_PAYLOAD") --write-out $'\n\036dsh-http:%{http_code}\037\n' \
--config - "$BASE_URL" <<< "header = \"Authorization: Bearer $DEEPSEEK_API_KEY\""
)
curl_pid=$!
DSH_ACTIVE_CURL_PID=$curl_pid
((DSH_INTERRUPTED)) && dsh_cancel_active_curl
while IFS= read -r line <&"$stream_fd"; do
line=${line%$'\r'}
if [[ $line == $'\036dsh-http:'*$'\037' ]]; then
http_status=${line#$'\036dsh-http:'}
http_status=${http_status%$'\037'}
continue
fi
case $line in
data:*)
data=${line#data:}
data=${data# }
[[ -z $data || $data == '[DONE]' ]] || dsh_stream_consume_event "$data" || :
;;
''|event:*|id:*|retry:*|:*) ;;
*) DSH_STREAM_ERROR_BODY+="${DSH_STREAM_ERROR_BODY:+$'\n'}$line" ;;
esac
done
exec {stream_fd}<&-
wait "$curl_pid"
curl_status=$?
if ((DSH_INTERRUPTED)); then
wait "$curl_pid" 2>/dev/null || :
fi
DSH_ACTIVE_CURL_PID=''
((DSH_STREAM_CONTENT_SEEN)) && printf '\n'
((DSH_SHOW_THINKING && DSH_STREAM_REASONING_SEEN)) && printf '\n' >&2
if ((DSH_INTERRUPTED)); then
dsh_log '[interrupted] Request cancelled.'
return 130
fi
if ((curl_status != 0)); then
dsh_log "curl stream failed (exit code $curl_status); no tools executed for this request."
return 1
fi
if [[ $http_status != 200 ]]; then
dsh_report_http_error 'API' "$http_status" "$DSH_STREAM_ERROR_BODY"
return $?
fi
if [[ -n $DSH_STREAM_ERROR_BODY && -z $DSH_STREAM_FINISH ]]; then
dsh_report_http_error 'API stream' "$http_status" "$DSH_STREAM_ERROR_BODY"
return $?
fi
[[ -z $DSH_STREAM_ERROR ]] || {
dsh_log "$DSH_STREAM_ERROR"
return 1
}
dsh_stream_build_response
}
dsh_complete() {
local response status
if ((DSH_STREAM)); then
dsh_complete_stream
return $?
fi
dsh_build_payload "$DSH_HISTORY" false "$DSH_MAX_TOKENS"
dsh_log "$DSH_STATUS_RUNNING"
dsh_api_request "$DSH_PAYLOAD"
status=$?
if ((status != 0)); then
((status == 130)) && return 130
dsh_log "curl request failed (exit code $DSH_CURL_STATUS); no tools executed for this request."
return 1
fi
response=$DSH_RESPONSE
status=$DSH_HTTP_STATUS
if [[ $status != 200 ]]; then
dsh_report_http_error 'API' "$status" "$response"
return $?
fi
dsh_parse "$response" || {
dsh_log 'API returned invalid or unsupported JSON (Bash cannot represent NUL characters).'
return 1
}
status=${DSH_VALUE[root/choices/0/finish_reason]}
[[ $status == stop || $status == tool_calls ]] || {
dsh_log "Incomplete API response (finish_reason=$status); no tools executed for this request. If length, increase DSH_MAX_TOKENS."
return 1
}
[[ ${DSH_TYPE[root/choices/0/message]} == object && ${DSH_VALUE[root/choices/0/message/role]} == assistant ]] || {
dsh_log 'API response is missing an assistant message.'
return 1
}
# Preserve the original assistant JSON, including ALL reasoning_content.
dsh_history_append "${DSH_RAW[root/choices/0/message]}" assistant
[[ -z ${DSH_VALUE[root/choices/0/message/content]} || ${DSH_TYPE[root/choices/0/message/content]} == null ]] || \
printf '%s\n' "${DSH_VALUE[root/choices/0/message/content]}"
if ((DSH_SHOW_THINKING)) && [[ ${DSH_TYPE[root/choices/0/message/reasoning_content]} == string ]]; then
dsh_log "${DSH_VALUE[root/choices/0/message/reasoning_content]}"
fi
}
# ---- Agent loop -------------------------------------------------------------
dsh_complete_with_recovery() {
local status retries=0
while :; do
dsh_complete
status=$?
((status == 0)) && return 0
((status == 130)) && return 130
((status == 3)) || return 1
if ((!DSH_AUTO_COMPACT)); then
dsh_log '[compact] Overflow recovery is disabled by DSH_AUTO_COMPACT=0.'
return 1
fi
if ((retries >= DSH_MAX_OVERFLOW_RETRIES)); then
dsh_log '[compact] Context-overflow retry limit reached.'
return 1
fi
((retries+=1))
dsh_log "[compact] Attempting context-overflow recovery ($retries/$DSH_MAX_OVERFLOW_RETRIES)."
dsh_compact_after_overflow
status=$?
((status == 130)) && return 130
if ((status != 0)); then
dsh_log '[compact] No safe history reduction was available for overflow recovery.'
return 1
fi
done
}
dsh_collect_tool_calls() {
local i path id name
DSH_CALL_IDS=()
DSH_CALL_NAMES=()
DSH_CALL_ARGUMENTS=()
for ((i=0; ; i++)); do
path=root/choices/0/message/tool_calls/$i
[[ -n ${DSH_TYPE[$path]} ]] || break
id=${DSH_VALUE[$path/id]}
name=${DSH_VALUE[$path/function/name]}
if [[ -z $id || -z $name || ${DSH_TYPE[$path/function/arguments]} != string ]]; then
dsh_log 'API returned an incomplete tool call.'
return 1
fi
DSH_CALL_IDS+=("$id")
DSH_CALL_NAMES+=("$name")
DSH_CALL_ARGUMENTS+=("${DSH_VALUE[$path/function/arguments]}")
done
}
dsh_dispatch_tool() {
local name=$1 arguments=$2
dsh_log "[$name] ${arguments:0:2000}"
if ! dsh_parse "$arguments" || [[ ${DSH_TYPE[root]} != object ]]; then
DSH_RESULT='Tool error: arguments must be a valid JSON object (no NUL)'
elif [[ $name == bash && ${DSH_TYPE[root/command]} == string ]]; then
DSH_TOOL_RUNNING=1
dsh_bash "${DSH_VALUE[root/command]}" || :
DSH_TOOL_RUNNING=0
elif [[ $name == str_replace_editor &&
${DSH_TYPE[root/command]} == string &&
${DSH_TYPE[root/path]} == string ]]; then
dsh_editor || :
else
DSH_RESULT="Tool error: unknown tool or missing string command/path: $name"
fi
dsh_clip "$DSH_RESULT"
dsh_log "$DSH_RESULT"
}
dsh_append_tool_result() {
local id=$1 result=$2 quoted_id
dsh_quote "$id"
quoted_id=$DSH_QUOTED
dsh_quote "$result"
dsh_history_append "{\"role\":\"tool\",\"tool_call_id\":$quoted_id,\"content\":$DSH_QUOTED}" tool
}
dsh_turn() {
local prompt=$1 step i
DSH_INTERRUPTED=0
dsh_quote "$prompt"
dsh_history_append "{\"role\":\"user\",\"content\":$DSH_QUOTED}" user
for ((step=0; step ' prompt; then
:
else
status=$?
if ((status == 130)); then
DSH_INTERRUPTED=0
continue
fi
printf '\n' >&2
return 0
fi
case $prompt in
/exit|/quit) return 0 ;;
/compact) dsh_run_manual_compaction; status=$? ;;
/clear)
dsh_shell_close
dsh_history_reset
dsh_session_save; status=$?
;;
'') continue ;;
*) dsh_turn "$prompt"; status=$? ;;
esac
case $status in
0) ;;
130) DSH_INTERRUPTED=0 ;;
*) return "$status" ;;
esac
done
}
dsh_main() {
local prompt='' cwd=$DSH_CWD option interactive=0
while (($#)); do
option=$1
shift
case $option in
-h|--help)
dsh_help
return 0
;;
--)
prompt+="${prompt:+ }$*"
break
;;
-) prompt+="${prompt:+ }-" ;;
-*)
dsh_log "Unknown option: $option (edit the configuration block in d.sh)"
return 2
;;
*) prompt+="${prompt:+ }$option" ;;
esac
done
DSH_SETUP_CHANGED=0
dsh_configure_connection || return 2
dsh_validate_config || return 2
dsh_persist_connection || return 2
BASE_URL=${BASE_URL%/}
[[ $BASE_URL == */chat/completions ]] || BASE_URL+=/chat/completions
cd -- "$cwd" || {
dsh_log "Cannot enter working directory: $cwd"
return 2
}
trap dsh_shell_close EXIT
trap dsh_interrupt INT
trap 'exit 143' TERM
dsh_tools
dsh_history_reset
dsh_session_load
DSH_SESSION_READY=1
[[ -n $prompt || ! -t 0 ]] || interactive=1
if [[ $prompt == - ]] || { [[ -z $prompt ]] && ((!interactive)); }; then
IFS= read -r -d '' prompt || :
fi
dsh_log "d.sh | $MODEL_NAME | $PWD"
if ((DSH_SESSION_LOADED)); then
dsh_log "Resumed $DSH_SESSION_LOADED message(s) from $DSH_SESSION_FILE."
fi
if ((interactive)); then
dsh_run_interactive
return $?
fi
[[ -n $prompt ]] || {
dsh_log 'Task text is empty.'
return 2
}
dsh_turn "$prompt"
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
dsh_main "$@"
exit $?
fi